From b1cdb1c39c9b87c9893d8120d8d4a6a512ce0fc1 Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 19 Aug 2024 07:10:29 -0400 Subject: [PATCH] add tests and custom labware support --- .../localization/en/starting_deck_state.json | 5 +- .../StartingDeckState/DeckSetupTools.tsx | 213 ++-- .../pages/StartingDeckState/LabwareTools.tsx | 187 ++- .../__tests__/DeckSetupTools.test.tsx | 132 ++ .../__tests__/LabwareTools.test.tsx | 101 ++ .../__tests__/utils.test.tsx | 44 + .../src/pages/StartingDeckState/constants.ts | 13 + .../src/pages/StartingDeckState/index.tsx | 1 + .../src/pages/StartingDeckState/utils.ts | 20 +- .../src/step-forms/actions/additionalItems.ts | 6 +- .../src/step-forms/actions/thunks.ts | 39 + ...fixture_corning_96_wellplate_360_flat.json | 1063 +++++++++++++++++ ...fixture_universal_flat_bottom_adapter.json | 151 +++ shared-data/labware/fixtures/2/index.ts | 18 +- 14 files changed, 1870 insertions(+), 123 deletions(-) create mode 100644 protocol-designer/src/pages/StartingDeckState/__tests__/DeckSetupTools.test.tsx create mode 100644 protocol-designer/src/pages/StartingDeckState/__tests__/LabwareTools.test.tsx create mode 100644 protocol-designer/src/pages/StartingDeckState/__tests__/utils.test.tsx create mode 100644 protocol-designer/src/step-forms/actions/thunks.ts create mode 100644 shared-data/labware/fixtures/2/fixture_corning_96_wellplate_360_flat.json create mode 100644 shared-data/labware/fixtures/2/fixture_universal_flat_bottom_adapter.json diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index 3ef215f4244..9730fccd669 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -1,11 +1,13 @@ { - "adapter": "Adapter", "adapter_compatible_lab": "Adapter compatible labware", + "adapter": "Adapter", "add_fixture": "Add a fixture", "add_labware": "Add labware", "add_module": "Add a module", "aluminumBlock": "Aluminum block", "clear": "Clear", + "custom_labware": "Add custom labware", + "custom": "Custom labware definitions", "customize_slot": "Customize slot {{slotName}}", "deck_hardware": "Deck hardware", "done": "Done", @@ -13,6 +15,7 @@ "labware": "Labware", "reservoir": "Reservoir", "starting_deck_state": "Starting deck state", + "tipRack": "Tip rack", "tiprack": "Tiprack", "tubeRack": "Tube rack", "wellPlate": "Well plate" diff --git a/protocol-designer/src/pages/StartingDeckState/DeckSetupTools.tsx b/protocol-designer/src/pages/StartingDeckState/DeckSetupTools.tsx index 40a8f125b07..79ac923a16e 100644 --- a/protocol-designer/src/pages/StartingDeckState/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/StartingDeckState/DeckSetupTools.tsx @@ -11,11 +11,13 @@ import { Toolbox, } from '@opentrons/components' import { + MAGNETIC_BLOCK_V1, + MODULE_MODELS, OT2_ROBOT_TYPE, - WASTE_CHUTE_CUTOUT, getModuleDisplayName, getModuleType, } from '@opentrons/shared-data' + import { getRobotType } from '../../file-data/selectors' import { createDeckFixture, @@ -24,56 +26,80 @@ import { import { createModule, deleteModule } from '../../step-forms/actions' import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations' import { createContainer, deleteContainer } from '../../labware-ingred/actions' -import { getEnableAbsorbanceReader } from '../../feature-flags/selectors' -import { FIXTURES } from './constants' +import { + getEnableAbsorbanceReader, + getEnableMoam, +} from '../../feature-flags/selectors' +import { createContainerAboveModule } from '../../step-forms/actions/thunks' +import { + FIXTURES, + MAX_MAGNETIC_BLOCKS, + MAX_MOAM_MODULES, + MOAM_MODELS, + MOAM_MODELS_WITH_FF, +} from './constants' import { getModuleModelsBySlot } from './utils' import { LabwareTools } from './LabwareTools' -import type { DeckSlotId, ModuleModel } from '@opentrons/shared-data' +import type { CutoutId, DeckSlotId, ModuleModel } from '@opentrons/shared-data' +import type { DeckFixture } from '../../step-forms/actions/additionalItems' import type { Fixture } from './constants' import type { ThunkDispatch } from '../../types' interface DeckSetupToolsProps { + cutoutId: CutoutId slot: DeckSlotId onCloseClick: () => void } export const DeckSetupTools = (props: DeckSetupToolsProps): JSX.Element => { - const { slot, onCloseClick } = props + const { slot, onCloseClick, cutoutId } = props const { t } = useTranslation(['starting_deck_state', 'shared']) const robotType = useSelector(getRobotType) const dispatch = useDispatch>() const enableAbsorbanceReader = useSelector(getEnableAbsorbanceReader) + const enableMoam = useSelector(getEnableMoam) const deckSetup = useSelector(getDeckSetupForActiveItem) const { labware: deckSetupLabware, modules: deckSetupModules, additionalEquipmentOnDeck, } = deckSetup + const hasTrash = Object.values(additionalEquipmentOnDeck).some( + ae => ae.name === 'trashBin' + ) const createdModuleForSlot = Object.values(deckSetupModules).find( module => module.slot === slot ) - const createdParentLabwareForSlot = Object.values(deckSetupLabware).find( + const createdLabwareForSlot = Object.values(deckSetupLabware).find( lw => lw.slot === slot || lw.slot === createdModuleForSlot?.id ) - const createdChildLabwareForSlot = Object.values(deckSetupLabware).find(lw => + const createdNestedLabwareForSlot = Object.values(deckSetupLabware).find(lw => Object.keys(deckSetupLabware).includes(lw.slot) ) - const createFixtureForSlot = Object.values(additionalEquipmentOnDeck).find( + const createFixtureForSlots = Object.values(additionalEquipmentOnDeck).filter( ae => ae.location?.split('cutout')[1] === slot ) + const preSelectedFixture = + createFixtureForSlots != null && createFixtureForSlots.length === 2 + ? ('wasteChuteAndStagingArea' as Fixture) + : (createFixtureForSlots[0]?.name as Fixture) + const [selectedHardware, setHardware] = React.useState< ModuleModel | Fixture | null - >(null) + >(createdModuleForSlot?.model ?? preSelectedFixture ?? null) + const [selecteLabwareDefURI, setSelectedLabwareDefURI] = React.useState< string | null - >(null) + >(createdLabwareForSlot?.labwareDefURI ?? null) const [ nestedSelectedLabwareDefURI, setNestedSelectedLabwareDefURI, - ] = React.useState(null) + ] = React.useState( + createdNestedLabwareForSlot?.labwareDefURI ?? null + ) const moduleModels = getModuleModelsBySlot( enableAbsorbanceReader, robotType, @@ -112,66 +138,87 @@ export const DeckSetupTools = (props: DeckSetupToolsProps): JSX.Element => { }, } - // TODO - this needs to be a thunk so we can grab the state for the slot Ids - // that are the module id or adapter id + const handleClear = (): void => { + // clear module from slot + if (createdModuleForSlot != null) { + dispatch(deleteModule(createdModuleForSlot.id)) + } + // clear fixture(s) from slot + if (createFixtureForSlots.length > 0) { + createFixtureForSlots.forEach(fixture => + dispatch(deleteDeckFixture(fixture.id)) + ) + } + // clear labware from slot + if (createdLabwareForSlot != null) { + dispatch(deleteContainer({ labwareId: createdLabwareForSlot.id })) + } + // clear nested labware from slot + if (createdNestedLabwareForSlot != null) { + dispatch(deleteContainer({ labwareId: createdNestedLabwareForSlot.id })) + } + } + const handleConfirm = (): void => { - if (selectedHardware != null) { - if (FIXTURES.includes(selectedHardware as Fixture)) { - if (selectedHardware === 'wasteChuteAndStagingArea') { - dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_CUTOUT)) - dispatch(createDeckFixture('stagingArea', 'cutoutD3')) - } else { - dispatch( - createDeckFixture( - selectedHardware as 'wasteChute' | 'trashBin' | 'stagingArea', - slot - ) - ) - } + // clear entities first before recreating them + handleClear() + const fixture = FIXTURES.includes(selectedHardware as Fixture) + ? (selectedHardware as DeckFixture | 'wasteChuteAndStagingArea') + : undefined + const moduleModel = MODULE_MODELS.includes(selectedHardware as ModuleModel) + ? (selectedHardware as ModuleModel) + : undefined + + if (fixture != null) { + // create fixture(s) + if (fixture === 'wasteChuteAndStagingArea') { + dispatch(createDeckFixture('wasteChute', cutoutId)) + dispatch(createDeckFixture('stagingArea', cutoutId)) } else { - dispatch( - createModule({ - slot: slot, - type: getModuleType(selectedHardware as ModuleModel), - model: selectedHardware as ModuleModel, - }) - ) + dispatch(createDeckFixture(fixture, cutoutId)) } } - if (selecteLabwareDefURI != null) { - const parentLabwareSlot = - createdModuleForSlot != null ? createdModuleForSlot.slot : slot + if (moduleModel != null) { + // create module dispatch( - createContainer({ - slot: parentLabwareSlot, - labwareDefURI: selecteLabwareDefURI, + createModule({ + slot: slot, + type: getModuleType(moduleModel), + model: moduleModel, }) ) } - if (nestedSelectedLabwareDefURI != null) { + if (moduleModel == null && selecteLabwareDefURI != null) { + // create adapter + labware on deck dispatch( createContainer({ - slot: slot, - labwareDefURI: nestedSelectedLabwareDefURI, + slot, + labwareDefURI: + nestedSelectedLabwareDefURI == null + ? selecteLabwareDefURI + : nestedSelectedLabwareDefURI, + adapterUnderLabwareDefURI: + nestedSelectedLabwareDefURI == null + ? undefined + : selecteLabwareDefURI, + }) + ) + } + if (moduleModel != null && selecteLabwareDefURI != null) { + // create adapter + labware on module + dispatch( + createContainerAboveModule({ + slot, + labwareDefURI: selecteLabwareDefURI, + nestedLabwareDefURI: nestedSelectedLabwareDefURI ?? undefined, }) ) } + onCloseClick() } - const handleClear = (): void => { - if (createdModuleForSlot != null) { - dispatch(deleteModule(createdModuleForSlot.id)) - } - if (createFixtureForSlot != null) { - dispatch(deleteDeckFixture(createFixtureForSlot.id)) - } - if (createdParentLabwareForSlot != null) { - dispatch(deleteContainer({ labwareId: createdParentLabwareForSlot.id })) - } - if (createdChildLabwareForSlot != null) { - dispatch(deleteContainer({ labwareId: createdChildLabwareForSlot.id })) - } + const handleResetToolbox = (): void => { setHardware(null) setSelectedLabwareDefURI(null) setNestedSelectedLabwareDefURI(null) @@ -182,8 +229,13 @@ export const DeckSetupTools = (props: DeckSetupToolsProps): JSX.Element => { width="400px" title={t('customize_slot', { slotName: slot })} closeButtonText={t('clear')} - onCloseClick={handleClear} - onConfirmClick={handleConfirm} + onCloseClick={() => { + handleClear() + handleResetToolbox() + }} + onConfirmClick={() => { + handleConfirm() + }} confirmButtonText={t('done')} > @@ -200,18 +252,39 @@ export const DeckSetupTools = (props: DeckSetupToolsProps): JSX.Element => { {t('add_module')} - {moduleModels.map(model => ( - { - setHardware(model) - setSelectedLabwareDefURI(null) - }} - isSelected={model === selectedHardware} - /> - ))} + {moduleModels.map(model => { + const modelSomewhereOnDeck = Object.values( + deckSetupModules + ).filter( + module => module.model === model && module.slot !== slot + ) + const moamModels = enableMoam + ? MOAM_MODELS + : MOAM_MODELS_WITH_FF + const maxMoamModel = + model === MAGNETIC_BLOCK_V1 + ? MAX_MAGNETIC_BLOCKS + : MAX_MOAM_MODULES + + return ( + { + setHardware(model) + setSelectedLabwareDefURI(null) + }} + isSelected={model === selectedHardware} + /> + ) + })} {robotType === OT2_ROBOT_TYPE || fixtures.length === 0 ? null : ( { {fixtures.map(fixture => ( { selectedNestedSelectedLabwareDefURI, } = props const { t } = useTranslation(['starting_deck_state', 'shared']) + const robotType = useSelector(getRobotType) + const dispatch = useDispatch>() const permittedTipracks = useSelector(stepFormSelectors.getPermittedTipracks) const pipetteEntities = useSelector(getPipetteEntities) const has96Channel = getHas96Channel(pipetteEntities) + const customLabwareDefs = useSelector(getCustomLabwareDefsByURI) const deckSetup = useSelector(stepFormSelectors.getInitialDeckSetup) const defs = getOnlyLatestDefs() - const modulesById = deckSetup.modules + // TODO(ja, 8/16/24): We are always filtering recommended labware, check with designs + // where to add the filter checkbox/button + const [filterRecommended, setFilterRecommended] = React.useState( + true + ) + const [selectedCategory, setSelectedCategory] = React.useState( + null + ) + const [filterHeight, setFilterHeight] = React.useState(false) + const modulesById = deckSetup.modules const moduleModel = MODULE_MODELS.includes(selectedHardware as ModuleModel) ? (selectedHardware as ModuleModel) : null @@ -79,6 +102,7 @@ export const LabwareTools = (props: LabwareToolsProps): JSX.Element => { const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map( moduleId => modulesById[moduleId] ) + // for OT-2 usage only due to H-S collisions const isNextToHeaterShaker = initialModules.some( hardwareModule => hardwareModule.type === HEATERSHAKER_MODULE_TYPE && @@ -87,19 +111,11 @@ export const LabwareTools = (props: LabwareToolsProps): JSX.Element => { // if you're adding labware to a module, check the recommended filter by default React.useEffect(() => { setFilterRecommended(moduleType != null) - setFilterHeight(isNextToHeaterShaker) - }, [moduleType, isNextToHeaterShaker]) - - // TODO: We are always filtering recommended labware, check with designs - // where to add the filter checkbox - const [filterRecommended, setFilterRecommended] = React.useState( - true - ) - const [selectedCategory, setSelectedCategory] = React.useState( - null - ) + if (robotType === OT2_ROBOT_TYPE) { + setFilterHeight(isNextToHeaterShaker) + } + }, [moduleType, isNextToHeaterShaker, robotType]) - const [filterHeight, setFilterHeight] = React.useState(false) const getLabwareCompatible = React.useCallback( (def: LabwareDefinition2) => { // assume that custom (non-standard) labware is (potentially) compatible @@ -122,6 +138,7 @@ export const LabwareTools = (props: LabwareToolsProps): JSX.Element => { const isAdapter = labwareDef.allowedRoles?.includes('adapter') const isAdapter96Channel = parameters.loadName === ADAPTER_96_CHANNEL + return ( (filterRecommended && !getLabwareIsRecommended(labwareDef, moduleModel)) || @@ -140,6 +157,10 @@ export const LabwareTools = (props: LabwareToolsProps): JSX.Element => { }, [filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot] ) + const customLabwareURIs: string[] = React.useMemo( + () => Object.keys(customLabwareDefs), + [customLabwareDefs] + ) const labwareByCategory = React.useMemo(() => { return reduce< LabwareDefByDefURI, @@ -164,6 +185,7 @@ export const LabwareTools = (props: LabwareToolsProps): JSX.Element => { {} ) }, [permittedTipracks]) + const populatedCategories: { [category: string]: boolean } = React.useMemo( () => ORDERED_CATEGORIES.reduce( @@ -180,6 +202,7 @@ export const LabwareTools = (props: LabwareToolsProps): JSX.Element => { ), [labwareByCategory, getIsLabwareFiltered] ) + return ( { {t('add_labware')} + {customLabwareURIs.length === 0 ? null : ( + { + setSelectedCategory(CUSTOM_CATEGORY) + }} + > + + + {customLabwareURIs.map((labwareURI, index) => ( + { + e.stopPropagation() + setSelectedLabwareDefURI(labwareURI) + }} + isSelected={labwareURI === selecteLabwareDefURI} + /> + ))} + + + + )} {ORDERED_CATEGORIES.map(category => { const isPopulated = populatedCategories[category] if (isPopulated) { @@ -212,6 +267,7 @@ export const LabwareTools = (props: LabwareToolsProps): JSX.Element => { const isFiltered = getIsLabwareFiltered(labwareDef) const labwareURI = getLabwareDefURI(labwareDef) const loadName = labwareDef.parameters.loadName + if (!isFiltered) { return ( { labwareURI === selecteLabwareDefURI } > - {getLabwareCompatibleWithAdapter( - loadName - ).map(nestedDefUri => { - const nestedDef = defs[nestedDefUri] - - return ( - { - e.stopPropagation() - setNestedSelectedLabwareDefURI( - nestedDefUri + {has96Channel && + loadName === ADAPTER_96_CHANNEL + ? permittedTipracks.map( + (tiprackDefUri, index) => { + const nestedDef = defs[tiprackDefUri] + return ( + { + e.stopPropagation() + setNestedSelectedLabwareDefURI( + tiprackDefUri + ) + }} + isSelected={ + tiprackDefUri === + selectedNestedSelectedLabwareDefURI + } + /> ) - }} - isSelected={ - nestedDefUri === - selectedNestedSelectedLabwareDefURI } - /> - ) - })} + ) + : getLabwareCompatibleWithAdapter( + loadName + ).map(nestedDefUri => { + const nestedDef = defs[nestedDefUri] + + return ( + { + e.stopPropagation() + setNestedSelectedLabwareDefURI( + nestedDefUri + ) + }} + isSelected={ + nestedDefUri === + selectedNestedSelectedLabwareDefURI + } + /> + ) + })} )} @@ -281,6 +366,28 @@ export const LabwareTools = (props: LabwareToolsProps): JSX.Element => { ) } })} + + + {t('custom_labware')} + + { + setSelectedCategory(CUSTOM_CATEGORY) + dispatch(createCustomLabwareDef(e)) + }} + /> + ) } + +const StyledLabel = styled.label` + text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; + text-align: ${TYPOGRAPHY.textAlignCenter}}; + display: inline-block; + cursor: pointer; + input[type='file'] { + display: none; + } +` diff --git a/protocol-designer/src/pages/StartingDeckState/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/StartingDeckState/__tests__/DeckSetupTools.test.tsx new file mode 100644 index 00000000000..6f699e88d5c --- /dev/null +++ b/protocol-designer/src/pages/StartingDeckState/__tests__/DeckSetupTools.test.tsx @@ -0,0 +1,132 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { fireEvent, screen } from '@testing-library/react' +import { FLEX_ROBOT_TYPE, fixture96Plate } from '@opentrons/shared-data' +import { i18n } from '../../../localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { deleteContainer } from '../../../labware-ingred/actions' +import { createModule, deleteModule } from '../../../step-forms/actions' +import { getRobotType } from '../../../file-data/selectors' +import { + getEnableAbsorbanceReader, + getEnableMoam, +} from '../../../feature-flags/selectors' +import { + createDeckFixture, + deleteDeckFixture, +} from '../../../step-forms/actions/additionalItems' +import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' +import { DeckSetupTools } from '../DeckSetupTools' +import { LabwareTools } from '../LabwareTools' + +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('../../../feature-flags/selectors') +vi.mock('../../../file-data/selectors') +vi.mock('../../../top-selectors/labware-locations') +vi.mock('../LabwareTools') +vi.mock('../../../labware-ingred/actions') +vi.mock('../../../step-forms/actions') +vi.mock('../../../step-forms/actions/additionalItems') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('DeckSetupTools', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + cutoutId: 'cutoutD3', + slot: 'D3', + onCloseClick: vi.fn(), + } + vi.mocked(LabwareTools).mockReturnValue(
mock labware tools
) + vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) + vi.mocked(getEnableAbsorbanceReader).mockReturnValue(true) + vi.mocked(getEnableMoam).mockReturnValue(true) + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ + labware: {}, + modules: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + }) + }) + it('should render the relevant modules and fixtures for slot D3 on Flex with tabs', () => { + render(props) + screen.getByText('Add a module') + screen.getByText('Add a fixture') + screen.getByText('Customize slot D3') + screen.getByText('Deck hardware') + screen.getByText('Labware') + screen.getByText('Absorbance Plate Reader Module GEN1') + screen.getByText('Heater-Shaker Module GEN1') + screen.getByText('Magnetic Block GEN1') + screen.getByText('Temperature Module GEN2') + screen.getByText('Staging area') + screen.getByText('Waste chute') + screen.getByText('Trash bin') + screen.getByText('Waste chute and staging area') + }) + it('should render the labware tab', () => { + render(props) + screen.getByText('Deck hardware') + // click on labware tab + fireEvent.click(screen.getByText('Labware')) + screen.getByText('mock labware tools') + }) + it('should clear the slot from all items when the clear cta is called', () => { + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ + labware: { + labId: { + slot: 'D3', + id: 'labId', + labwareDefURI: 'mockUri', + def: fixture96Plate as LabwareDefinition2, + }, + lab2: { + slot: 'labId', + id: 'labId2', + labwareDefURI: 'mockUri', + def: fixture96Plate as LabwareDefinition2, + }, + }, + pipettes: {}, + modules: { + mod: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + id: 'modId', + slot: 'D3', + moduleState: {} as any, + }, + }, + additionalEquipmentOnDeck: { + fixture: { name: 'stagingArea', id: 'mockId', location: 'cutoutD3' }, + }, + }) + render(props) + fireEvent.click(screen.getByText('Clear')) + expect(vi.mocked(deleteContainer)).toHaveBeenCalledTimes(2) + expect(vi.mocked(deleteModule)).toHaveBeenCalled() + expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalled() + }) + it('should close and add h-s module when done is called', () => { + render(props) + fireEvent.click(screen.getByText('Heater-Shaker Module GEN1')) + fireEvent.click(screen.getByText('Done')) + expect(props.onCloseClick).toHaveBeenCalled() + expect(vi.mocked(createModule)).toHaveBeenCalled() + }) + it('should close and add waste chute and staging area when done is called', () => { + render(props) + fireEvent.click(screen.getByText('Waste chute and staging area')) + fireEvent.click(screen.getByText('Done')) + expect(props.onCloseClick).toHaveBeenCalled() + expect(vi.mocked(createDeckFixture)).toHaveBeenCalledTimes(2) + }) +}) diff --git a/protocol-designer/src/pages/StartingDeckState/__tests__/LabwareTools.test.tsx b/protocol-designer/src/pages/StartingDeckState/__tests__/LabwareTools.test.tsx new file mode 100644 index 00000000000..f5eb6ac8ea5 --- /dev/null +++ b/protocol-designer/src/pages/StartingDeckState/__tests__/LabwareTools.test.tsx @@ -0,0 +1,101 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { + getInitialDeckSetup, + getPermittedTipracks, + getPipetteEntities, +} from '../../../step-forms/selectors' +import { + FLEX_ROBOT_TYPE, + HEATERSHAKER_MODULE_V1, + fixtureP1000SingleV2Specs, + fixtureTiprack1000ul, +} from '@opentrons/shared-data' +import { getHas96Channel } from '../../../utils' +import { getCustomLabwareDefsByURI } from '../../../labware-defs/selectors' +import { getRobotType } from '../../../file-data/selectors' +import { LabwareTools } from '../LabwareTools' +import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data' + +vi.mock('../../../utils') +vi.mock('../../../step-forms/selectors') +vi.mock('../../../file-data/selectors') +vi.mock('../../../labware-defs/selectors') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('LabwareTools', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + slot: 'D3', + selectedHardware: null, + setSelectedLabwareDefURI: vi.fn(), + selecteLabwareDefURI: null, + setNestedSelectedLabwareDefURI: vi.fn(), + selectedNestedSelectedLabwareDefURI: null, + } + vi.mocked(getCustomLabwareDefsByURI).mockReturnValue({}) + vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) + vi.mocked(getPermittedTipracks).mockReturnValue([]) + vi.mocked(getPipetteEntities).mockReturnValue({ + pip: { + tiprackDefURI: ['mockTipUri'], + spec: fixtureP1000SingleV2Specs as PipetteV2Specs, + name: 'p1000_single_flex', + id: 'mockPipId', + tiprackLabwareDef: [fixtureTiprack1000ul as LabwareDefinition2], + }, + }) + vi.mocked(getHas96Channel).mockReturnValue(false) + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: {}, + labware: {}, + pipettes: {}, + additionalEquipmentOnDeck: {}, + }) + }) + + it('renders an empty slot with all the labware options', () => { + render(props) + screen.getByText('Add labware') + screen.getByText('Tube rack') + screen.getByText('Well plate') + screen.getByText('Reservoir') + screen.getByText('Aluminum block') + screen.getByText('Adapter') + // click and expand well plate accordion + fireEvent.click(screen.getAllByTestId('ListButton_noActive')[1]) + fireEvent.click( + screen.getByRole('label', { name: 'Corning 384 Well Plate' }) + ) + // set labware + expect(props.setSelectedLabwareDefURI).toHaveBeenCalled() + }) + it('renders slot with heater-shaker on it and selects an adapter and labware', () => { + props.selectedHardware = HEATERSHAKER_MODULE_V1 + props.selecteLabwareDefURI = 'fixture/opentrons_universal_flat_adapter/1' + render(props) + screen.getByText('Adapter') + fireEvent.click(screen.getAllByTestId('ListButton_noActive')[0]) + // set adapter + fireEvent.click( + screen.getByRole('label', { + name: 'Opentrons Universal Flat Heater-Shaker Adapter', + }) + ) + // set labware + screen.getByText('Adapter compatible labware') + fireEvent.click(screen.getAllByRole('label')[1]) + expect(props.setNestedSelectedLabwareDefURI).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/pages/StartingDeckState/__tests__/utils.test.tsx b/protocol-designer/src/pages/StartingDeckState/__tests__/utils.test.tsx new file mode 100644 index 00000000000..86f01459653 --- /dev/null +++ b/protocol-designer/src/pages/StartingDeckState/__tests__/utils.test.tsx @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest' +import { + FLEX_ROBOT_TYPE, + MAGNETIC_BLOCK_V1, + OT2_ROBOT_TYPE, + THERMOCYCLER_MODULE_V1, + THERMOCYCLER_MODULE_V2, +} from '@opentrons/shared-data' +import { getModuleModelsBySlot } from '../utils' +import { FLEX_MODULE_MODELS, OT2_MODULE_MODELS } from '../constants' + +describe('getModuleModelsBySlot', () => { + it('renders no modules for ot-2 middle slot', () => { + expect(getModuleModelsBySlot(false, OT2_ROBOT_TYPE, '5')).toEqual([]) + }) + it('renders all ot-2 modules for slot 7', () => { + expect(getModuleModelsBySlot(false, OT2_ROBOT_TYPE, '7')).toEqual( + OT2_MODULE_MODELS + ) + }) + it('renders ot-2 modules minus thermocyclers for slot 1', () => { + const noTC = OT2_MODULE_MODELS.filter( + model => + model !== THERMOCYCLER_MODULE_V1 && model !== THERMOCYCLER_MODULE_V2 + ) + expect(getModuleModelsBySlot(false, OT2_ROBOT_TYPE, '1')).toEqual(noTC) + }) + it('renders flex modules for middle slots', () => { + expect(getModuleModelsBySlot(false, FLEX_ROBOT_TYPE, 'B2')).toEqual([ + MAGNETIC_BLOCK_V1, + ]) + }) + it('renders all flex modules for B1', () => { + expect(getModuleModelsBySlot(false, FLEX_ROBOT_TYPE, 'B1')).toEqual( + FLEX_MODULE_MODELS + ) + }) + it('renders all flex modules for C1', () => { + const noTC = FLEX_MODULE_MODELS.filter( + model => model !== THERMOCYCLER_MODULE_V2 + ) + expect(getModuleModelsBySlot(false, FLEX_ROBOT_TYPE, 'C1')).toEqual(noTC) + }) +}) diff --git a/protocol-designer/src/pages/StartingDeckState/constants.ts b/protocol-designer/src/pages/StartingDeckState/constants.ts index 3617b42277d..292cc897b4c 100644 --- a/protocol-designer/src/pages/StartingDeckState/constants.ts +++ b/protocol-designer/src/pages/StartingDeckState/constants.ts @@ -14,6 +14,7 @@ import { MAGNETIC_BLOCK_TYPE, ABSORBANCE_READER_TYPE, } from '@opentrons/shared-data' + import type { ModuleModel, ModuleType } from '@opentrons/shared-data' export const FLEX_MODULE_MODELS: ModuleModel[] = [ @@ -91,3 +92,15 @@ export const RECOMMENDED_LABWARE_BY_MODULE: { [K in ModuleType]: string[] } = { ], [ABSORBANCE_READER_TYPE]: [], } + +export const MOAM_MODELS_WITH_FF: ModuleModel[] = [TEMPERATURE_MODULE_V2] +export const MOAM_MODELS: ModuleModel[] = [ + TEMPERATURE_MODULE_V2, + HEATERSHAKER_MODULE_V1, + MAGNETIC_BLOCK_V1, +] + +export const MAX_MOAM_MODULES = 7 +// limiting 10 instead of 11 to make space for a single default tiprack +// to be auto-generated +export const MAX_MAGNETIC_BLOCKS = 10 diff --git a/protocol-designer/src/pages/StartingDeckState/index.tsx b/protocol-designer/src/pages/StartingDeckState/index.tsx index b9cfd69df66..04942c4f3f7 100644 --- a/protocol-designer/src/pages/StartingDeckState/index.tsx +++ b/protocol-designer/src/pages/StartingDeckState/index.tsx @@ -144,6 +144,7 @@ export function StartingDeckState(): JSX.Element { onCloseClick={() => { setZoomInOnSlot(null) }} + cutoutId={zoomIn.cutoutId} slot={zoomIn.slot} /> ) : ( diff --git a/protocol-designer/src/pages/StartingDeckState/utils.ts b/protocol-designer/src/pages/StartingDeckState/utils.ts index 2dae434ef8c..d94012fc13f 100644 --- a/protocol-designer/src/pages/StartingDeckState/utils.ts +++ b/protocol-designer/src/pages/StartingDeckState/utils.ts @@ -1,5 +1,7 @@ import { FLEX_ROBOT_TYPE, + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, + MAGNETIC_BLOCK_V1, OT2_ROBOT_TYPE, THERMOCYCLER_MODULE_TYPE, THERMOCYCLER_MODULE_V2, @@ -10,6 +12,7 @@ import { OT2_MODULE_MODELS, RECOMMENDED_LABWARE_BY_MODULE, } from './constants' + import type { AddressableAreaName, CutoutFixture, @@ -51,20 +54,27 @@ export function getModuleModelsBySlot( case FLEX_ROBOT_TYPE: { if (slot !== 'B1' && !FLEX_MIDDLE_SLOTS.includes(slot)) { moduleModels = FLEX_MODULE_MODELS.filter( - model => model !== 'thermocyclerModuleV2' + model => model !== THERMOCYCLER_MODULE_V2 ) } if (FLEX_MIDDLE_SLOTS.includes(slot)) { moduleModels = FLEX_MODULE_MODELS.filter( - model => model === 'magneticBlockV1' + model => model === MAGNETIC_BLOCK_V1 ) } + if ( + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes( + slot as AddressableAreaName + ) + ) { + moduleModels = [] + } break } case OT2_ROBOT_TYPE: { if (OT2_MIDDLE_SLOTS.includes(slot)) { moduleModels = [] - } else if (slot !== '10') { + } else if (slot !== '7') { moduleModels = OT2_MODULE_MODELS.filter( model => getModuleType(model) !== THERMOCYCLER_MODULE_TYPE ) @@ -76,11 +86,13 @@ export function getModuleModelsBySlot( } return moduleModels } + export const getLabwareIsRecommended = ( def: LabwareDefinition2, moduleModel?: ModuleModel | null ): boolean => { - // special-casing the thermocycler module V2 recommended labware + // special-casing the thermocycler module V2 recommended labware since the thermocyclerModuleTypes + // have different recommended labware const moduleType = moduleModel != null ? getModuleType(moduleModel) : null if (moduleModel === THERMOCYCLER_MODULE_V2) { return ( diff --git a/protocol-designer/src/step-forms/actions/additionalItems.ts b/protocol-designer/src/step-forms/actions/additionalItems.ts index 97fa27cab0e..f9dbd78c45d 100644 --- a/protocol-designer/src/step-forms/actions/additionalItems.ts +++ b/protocol-designer/src/step-forms/actions/additionalItems.ts @@ -7,17 +7,19 @@ export interface ToggleIsGripperRequiredAction { export const toggleIsGripperRequired = (): ToggleIsGripperRequiredAction => ({ type: 'TOGGLE_IS_GRIPPER_REQUIRED', }) + +export type DeckFixture = 'wasteChute' | 'stagingArea' | 'trashBin' export interface CreateDeckFixtureAction { type: 'CREATE_DECK_FIXTURE' payload: { - name: 'wasteChute' | 'stagingArea' | 'trashBin' + name: DeckFixture id: string location: string } } export const createDeckFixture = ( - name: 'wasteChute' | 'stagingArea' | 'trashBin', + name: DeckFixture, location: string ): CreateDeckFixtureAction => ({ type: 'CREATE_DECK_FIXTURE', diff --git a/protocol-designer/src/step-forms/actions/thunks.ts b/protocol-designer/src/step-forms/actions/thunks.ts new file mode 100644 index 00000000000..0a0f5f17227 --- /dev/null +++ b/protocol-designer/src/step-forms/actions/thunks.ts @@ -0,0 +1,39 @@ +import { createContainer } from '../../labware-ingred/actions' +import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations' + +import type { DeckSlotId } from '@opentrons/shared-data' +import type { ThunkAction } from '../../types' +import type { + CreateContainerAction, + RenameLabwareAction, +} from '../../labware-ingred/actions' + +export interface CreateContainerAboveModuleArgs { + slot: DeckSlotId + labwareDefURI: string + nestedLabwareDefURI?: string +} + +export const createContainerAboveModule: ( + args: CreateContainerAboveModuleArgs +) => ThunkAction = args => ( + dispatch, + getState +) => { + const { slot, labwareDefURI, nestedLabwareDefURI } = args + const state = getState() + const deckSetup = getDeckSetupForActiveItem(state) + const modules = deckSetup.modules + + const moduleId = Object.values(modules).find(module => module.slot === slot) + ?.id + dispatch( + createContainer({ + slot: moduleId, + labwareDefURI: + nestedLabwareDefURI == null ? labwareDefURI : nestedLabwareDefURI, + adapterUnderLabwareDefURI: + nestedLabwareDefURI == null ? undefined : labwareDefURI, + }) + ) +} diff --git a/shared-data/labware/fixtures/2/fixture_corning_96_wellplate_360_flat.json b/shared-data/labware/fixtures/2/fixture_corning_96_wellplate_360_flat.json new file mode 100644 index 00000000000..b4beeb6abc9 --- /dev/null +++ b/shared-data/labware/fixtures/2/fixture_corning_96_wellplate_360_flat.json @@ -0,0 +1,1063 @@ +{ + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "brand": { + "brand": "Corning", + "brandId": [ + "3650", + "3916", + "3915", + "3361", + "3590", + "9018", + "3591", + "9017", + "3641", + "3628", + "3370", + "2507", + "2509", + "2503", + "3665", + "3600", + "3362", + "3917", + "3912", + "3925", + "3922", + "3596", + "3977", + "3598", + "3599", + "3585", + "3595", + "3300", + "3474" + ], + "links": [ + "https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Corning%C2%AE-96-well-Solid-Black-and-White-Polystyrene-Microplates/p/corning96WellSolidBlackAndWhitePolystyreneMicroplates" + ] + }, + "metadata": { + "displayName": "Corning 96 Well Plate 360 µL Flat", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.47, + "zDimension": 14.22 + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "gripForce": 15, + "gripHeightFromLabwareBottom": 12.2, + "wells": { + "H1": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 14.38, + "y": 11.24, + "z": 3.55 + }, + "G1": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 14.38, + "y": 20.24, + "z": 3.55 + }, + "F1": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 14.38, + "y": 29.24, + "z": 3.55 + }, + "E1": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 14.38, + "y": 38.24, + "z": 3.55 + }, + "D1": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 14.38, + "y": 47.24, + "z": 3.55 + }, + "C1": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 14.38, + "y": 56.24, + "z": 3.55 + }, + "B1": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 14.38, + "y": 65.24, + "z": 3.55 + }, + "A1": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 14.38, + "y": 74.24, + "z": 3.55 + }, + "H2": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 23.38, + "y": 11.24, + "z": 3.55 + }, + "G2": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 23.38, + "y": 20.24, + "z": 3.55 + }, + "F2": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 23.38, + "y": 29.24, + "z": 3.55 + }, + "E2": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 23.38, + "y": 38.24, + "z": 3.55 + }, + "D2": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 23.38, + "y": 47.24, + "z": 3.55 + }, + "C2": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 23.38, + "y": 56.24, + "z": 3.55 + }, + "B2": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 23.38, + "y": 65.24, + "z": 3.55 + }, + "A2": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 23.38, + "y": 74.24, + "z": 3.55 + }, + "H3": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 32.38, + "y": 11.24, + "z": 3.55 + }, + "G3": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 32.38, + "y": 20.24, + "z": 3.55 + }, + "F3": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 32.38, + "y": 29.24, + "z": 3.55 + }, + "E3": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 32.38, + "y": 38.24, + "z": 3.55 + }, + "D3": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 32.38, + "y": 47.24, + "z": 3.55 + }, + "C3": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 32.38, + "y": 56.24, + "z": 3.55 + }, + "B3": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 32.38, + "y": 65.24, + "z": 3.55 + }, + "A3": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 32.38, + "y": 74.24, + "z": 3.55 + }, + "H4": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 41.38, + "y": 11.24, + "z": 3.55 + }, + "G4": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 41.38, + "y": 20.24, + "z": 3.55 + }, + "F4": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 41.38, + "y": 29.24, + "z": 3.55 + }, + "E4": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 41.38, + "y": 38.24, + "z": 3.55 + }, + "D4": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 41.38, + "y": 47.24, + "z": 3.55 + }, + "C4": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 41.38, + "y": 56.24, + "z": 3.55 + }, + "B4": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 41.38, + "y": 65.24, + "z": 3.55 + }, + "A4": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 41.38, + "y": 74.24, + "z": 3.55 + }, + "H5": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 50.38, + "y": 11.24, + "z": 3.55 + }, + "G5": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 50.38, + "y": 20.24, + "z": 3.55 + }, + "F5": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 50.38, + "y": 29.24, + "z": 3.55 + }, + "E5": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 50.38, + "y": 38.24, + "z": 3.55 + }, + "D5": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 50.38, + "y": 47.24, + "z": 3.55 + }, + "C5": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 50.38, + "y": 56.24, + "z": 3.55 + }, + "B5": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 50.38, + "y": 65.24, + "z": 3.55 + }, + "A5": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 50.38, + "y": 74.24, + "z": 3.55 + }, + "H6": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 59.38, + "y": 11.24, + "z": 3.55 + }, + "G6": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 59.38, + "y": 20.24, + "z": 3.55 + }, + "F6": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 59.38, + "y": 29.24, + "z": 3.55 + }, + "E6": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 59.38, + "y": 38.24, + "z": 3.55 + }, + "D6": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 59.38, + "y": 47.24, + "z": 3.55 + }, + "C6": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 59.38, + "y": 56.24, + "z": 3.55 + }, + "B6": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 59.38, + "y": 65.24, + "z": 3.55 + }, + "A6": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 59.38, + "y": 74.24, + "z": 3.55 + }, + "H7": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 68.38, + "y": 11.24, + "z": 3.55 + }, + "G7": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 68.38, + "y": 20.24, + "z": 3.55 + }, + "F7": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 68.38, + "y": 29.24, + "z": 3.55 + }, + "E7": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 68.38, + "y": 38.24, + "z": 3.55 + }, + "D7": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 68.38, + "y": 47.24, + "z": 3.55 + }, + "C7": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 68.38, + "y": 56.24, + "z": 3.55 + }, + "B7": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 68.38, + "y": 65.24, + "z": 3.55 + }, + "A7": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 68.38, + "y": 74.24, + "z": 3.55 + }, + "H8": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 77.38, + "y": 11.24, + "z": 3.55 + }, + "G8": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 77.38, + "y": 20.24, + "z": 3.55 + }, + "F8": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 77.38, + "y": 29.24, + "z": 3.55 + }, + "E8": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 77.38, + "y": 38.24, + "z": 3.55 + }, + "D8": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 77.38, + "y": 47.24, + "z": 3.55 + }, + "C8": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 77.38, + "y": 56.24, + "z": 3.55 + }, + "B8": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 77.38, + "y": 65.24, + "z": 3.55 + }, + "A8": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 77.38, + "y": 74.24, + "z": 3.55 + }, + "H9": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 86.38, + "y": 11.24, + "z": 3.55 + }, + "G9": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 86.38, + "y": 20.24, + "z": 3.55 + }, + "F9": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 86.38, + "y": 29.24, + "z": 3.55 + }, + "E9": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 86.38, + "y": 38.24, + "z": 3.55 + }, + "D9": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 86.38, + "y": 47.24, + "z": 3.55 + }, + "C9": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 86.38, + "y": 56.24, + "z": 3.55 + }, + "B9": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 86.38, + "y": 65.24, + "z": 3.55 + }, + "A9": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 86.38, + "y": 74.24, + "z": 3.55 + }, + "H10": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 95.38, + "y": 11.24, + "z": 3.55 + }, + "G10": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 95.38, + "y": 20.24, + "z": 3.55 + }, + "F10": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 95.38, + "y": 29.24, + "z": 3.55 + }, + "E10": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 95.38, + "y": 38.24, + "z": 3.55 + }, + "D10": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 95.38, + "y": 47.24, + "z": 3.55 + }, + "C10": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 95.38, + "y": 56.24, + "z": 3.55 + }, + "B10": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 95.38, + "y": 65.24, + "z": 3.55 + }, + "A10": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 95.38, + "y": 74.24, + "z": 3.55 + }, + "H11": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 104.38, + "y": 11.24, + "z": 3.55 + }, + "G11": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 104.38, + "y": 20.24, + "z": 3.55 + }, + "F11": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 104.38, + "y": 29.24, + "z": 3.55 + }, + "E11": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 104.38, + "y": 38.24, + "z": 3.55 + }, + "D11": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 104.38, + "y": 47.24, + "z": 3.55 + }, + "C11": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 104.38, + "y": 56.24, + "z": 3.55 + }, + "B11": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 104.38, + "y": 65.24, + "z": 3.55 + }, + "A11": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 104.38, + "y": 74.24, + "z": 3.55 + }, + "H12": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 113.38, + "y": 11.24, + "z": 3.55 + }, + "G12": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 113.38, + "y": 20.24, + "z": 3.55 + }, + "F12": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 113.38, + "y": 29.24, + "z": 3.55 + }, + "E12": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 113.38, + "y": 38.24, + "z": 3.55 + }, + "D12": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 113.38, + "y": 47.24, + "z": 3.55 + }, + "C12": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 113.38, + "y": 56.24, + "z": 3.55 + }, + "B12": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 113.38, + "y": 65.24, + "z": 3.55 + }, + "A12": { + "depth": 10.67, + "shape": "circular", + "diameter": 6.86, + "totalLiquidVolume": 360, + "x": 113.38, + "y": 74.24, + "z": 3.55 + } + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + "metadata": { + "wellBottomShape": "flat" + } + } + ], + "parameters": { + "format": "96Standard", + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "corning_96_wellplate_360ul_flat" + }, + "namespace": "fixture", + "version": 2, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_universal_flat_adapter": { + "x": 0, + "y": 0, + "z": 10.22 + }, + "opentrons_aluminum_flat_bottom_plate": { + "x": 0, + "y": 0, + "z": 5.45 + } + } +} diff --git a/shared-data/labware/fixtures/2/fixture_universal_flat_bottom_adapter.json b/shared-data/labware/fixtures/2/fixture_universal_flat_bottom_adapter.json new file mode 100644 index 00000000000..d4b3d5177af --- /dev/null +++ b/shared-data/labware/fixtures/2/fixture_universal_flat_bottom_adapter.json @@ -0,0 +1,151 @@ +{ + "ordering": [], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "metadata": { + "displayName": "Opentrons Universal Flat Heater-Shaker Adapter", + "displayCategory": "adapter", + "displayVolumeUnits": "\u00b5L", + "tags": [] + }, + "dimensions": { + "xDimension": 111, + "yDimension": 75, + "zDimension": 12 + }, + "wells": {}, + "groups": [ + { + "metadata": {}, + "wells": [] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_universal_flat_adapter" + }, + "namespace": "fixture", + "version": 1, + "schemaVersion": 2, + "allowedRoles": ["adapter"], + "cornerOffsetFromSlot": { + "x": 8.5, + "y": 5.5, + "z": 0 + }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "A1": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 2.0, + "y": 0, + "z": 0 + } + }, + "B1": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 2.0, + "y": 0, + "z": 0 + } + }, + "C1": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 2.0, + "y": 0, + "z": 0 + } + }, + "D1": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 2.0, + "y": 0, + "z": 0 + } + }, + "A3": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": -2.0, + "y": 0, + "z": 0 + } + }, + "B3": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": -2.0, + "y": 0, + "z": 0 + } + }, + "C3": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": -2.0, + "y": 0, + "z": 0 + } + }, + "D3": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": -2.0, + "y": 0, + "z": 0 + } + } + } +} diff --git a/shared-data/labware/fixtures/2/index.ts b/shared-data/labware/fixtures/2/index.ts index 8fbf78ed51a..d339c0eeba6 100644 --- a/shared-data/labware/fixtures/2/index.ts +++ b/shared-data/labware/fixtures/2/index.ts @@ -1,8 +1,9 @@ -import fixture_12_trough_v2 from './fixture_12_trough_v2.json' import fixture_12_trough from './fixture_12_trough.json' +import fixture_12_trough_v2 from './fixture_12_trough_v2.json' import fixture_24_tuberack from './fixture_24_tuberack.json' -import fixture_96_plate from './fixture_96_plate.json' import fixture_384_plate from './fixture_384_plate.json' +import fixture_96_plate from './fixture_96_plate.json' +import fixture_calibration_block from './fixture_calibration_block.json' import fixture_flex_96_tiprack_1000ul from './fixture_flex_96_tiprack_1000ul.json' import fixture_flex_96_tiprack_adapter from './fixture_flex_96_tiprack_adapter.json' import fixture_irregular_example_1 from './fixture_irregular_example_1.json' @@ -10,17 +11,20 @@ import fixture_overlappy_wellplate from './fixture_overlappy_wellplate.json' import fixture_regular_example_1 from './fixture_regular_example_1.json' import fixture_regular_example_2 from './fixture_regular_example_2.json' import fixture_tiprack_10_ul from './fixture_tiprack_10_ul.json' -import fixture_tiprack_300_ul from './fixture_tiprack_300_ul.json' import fixture_tiprack_1000_ul from './fixture_tiprack_1000_ul.json' +import fixture_tiprack_300_ul from './fixture_tiprack_300_ul.json' import fixture_trash from './fixture_trash.json' -import fixture_calibration_block from './fixture_calibration_block.json' +import fixture_universal_flat_bottom_adapter from './fixture_universal_flat_bottom_adapter.json' +import fixture_corning_96_wellplate_360_flat from './fixture_corning_96_wellplate_360_flat.json' export { fixture_12_trough_v2, fixture_12_trough, fixture_24_tuberack, - fixture_96_plate, fixture_384_plate, + fixture_96_plate, + fixture_calibration_block, + fixture_corning_96_wellplate_360_flat, fixture_flex_96_tiprack_1000ul, fixture_flex_96_tiprack_adapter, fixture_irregular_example_1, @@ -28,8 +32,8 @@ export { fixture_regular_example_1, fixture_regular_example_2, fixture_tiprack_10_ul, - fixture_tiprack_300_ul, fixture_tiprack_1000_ul, + fixture_tiprack_300_ul, fixture_trash, - fixture_calibration_block, + fixture_universal_flat_bottom_adapter, }