Skip to content

Commit

Permalink
fix(protocol-designer): update logic to add modules and fixtures with…
Browse files Browse the repository at this point in the history
… plate reader (#17344)

* fix(protocol-designer): update logic to add modules and fixtures with plate reader
  • Loading branch information
koji authored Jan 28, 2025
1 parent c630b88 commit 8f8b055
Showing 6 changed files with 138 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -70,6 +70,15 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null {
subHeader={t('fixtures_replace')}
disabled={!hasTrash}
goBack={() => {
// Note this is avoid the following case issue.
// https://github.com/Opentrons/opentrons/pull/17344#pullrequestreview-2576591908
setValue(
'additionalEquipment',
additionalEquipment.filter(
ae => ae === 'gripper' || ae === 'trashBin'
)
)

goBack(1)
}}
proceed={handleProceed}
@@ -135,11 +144,18 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null {
filterOptions: getNumOptions(
numSlotsAvailable >= MAX_SLOTS
? MAX_SLOTS
: numSlotsAvailable + numStagingAreas
: numSlotsAvailable
),
onClick: (value: string) => {
const inputNum = parseInt(value)
let updatedStagingAreas = [...additionalEquipment]
const currentStagingAreas = additionalEquipment.filter(
additional => additional === 'stagingArea'
)
const otherEquipment = additionalEquipment.filter(
additional => additional !== 'stagingArea'
)
let updatedStagingAreas = currentStagingAreas
// let updatedStagingAreas = [...additionalEquipment]

if (inputNum > numStagingAreas) {
const difference = inputNum - numStagingAreas
@@ -148,13 +164,16 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null {
...Array(difference).fill(ae),
]
} else {
updatedStagingAreas = updatedStagingAreas.slice(
updatedStagingAreas = currentStagingAreas.slice(
0,
inputNum
)
}

setValue('additionalEquipment', updatedStagingAreas)
setValue('additionalEquipment', [
...otherEquipment,
...updatedStagingAreas,
])
},
}
return (
Original file line number Diff line number Diff line change
@@ -178,6 +178,7 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null {
) : null}
<Flex gridGap={SPACING.spacing4} flexWrap={WRAP}>
{filteredSupportedModules
.sort((moduleA, moduleB) => moduleA.localeCompare(moduleB))
.filter(module =>
enableAbsorbanceReader
? module
@@ -219,6 +220,9 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null {
gridGap={SPACING.spacing4}
>
{Object.entries(modules)
.sort(([, moduleA], [, moduleB]) =>
moduleA.model.localeCompare(moduleB.model)
)
.reduce<Array<FormModule & { count: number; key: string }>>(
(acc, [key, module]) => {
const existingModule = acc.find(
@@ -253,7 +257,9 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null {
},
dropdownType: 'neutral' as DropdownBorder,
filterOptions: getNumOptions(
numSlotsAvailable + module.count
module.model !== ABSORBANCE_READER_V1
? numSlotsAvailable + module.count
: numSlotsAvailable
),
}
return (
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { it, describe, expect } from 'vitest'
import {
FLEX_ROBOT_TYPE,
ABSORBANCE_READER_V1,
ABSORBANCE_READER_TYPE,
HEATERSHAKER_MODULE_TYPE,
HEATERSHAKER_MODULE_V1,
MAGNETIC_BLOCK_TYPE,
MAGNETIC_BLOCK_V1,
MAGNETIC_MODULE_V1,
MAGNETIC_MODULE_V2,
TEMPERATURE_MODULE_TYPE,
TEMPERATURE_MODULE_V1,
TEMPERATURE_MODULE_V2,
THERMOCYCLER_MODULE_TYPE,
THERMOCYCLER_MODULE_V1,
THERMOCYCLER_MODULE_V2,
} from '@opentrons/shared-data'
import { getNumSlotsAvailable, getTrashSlot } from '../utils'
@@ -36,18 +41,60 @@ describe('getNumSlotsAvailable', () => {
const result = getNumSlotsAvailable(null, [], 'gripper')
expect(result).toBe(0)
})
it('should return 1 for a non MoaM module', () => {

it('should return 1 for a non MoaM module - temperature module', () => {
const result = getNumSlotsAvailable(null, [], TEMPERATURE_MODULE_V1)
expect(result).toBe(1)
})

it('should return 1 for a non MoaM module - absorbance plate reader', () => {
const result = getNumSlotsAvailable(null, [], ABSORBANCE_READER_V1)
expect(result).toBe(1)
})

it('should return 1 for a non MoaM module - thermocycler v1', () => {
const result = getNumSlotsAvailable(null, [], THERMOCYCLER_MODULE_V1)
expect(result).toBe(1)
})

it('should return 1 for a non MoaM module - magnetic module v1', () => {
const result = getNumSlotsAvailable(null, [], MAGNETIC_MODULE_V1)
expect(result).toBe(1)
})

it('should return 1 for a non MoaM module - magnetic module v2', () => {
const result = getNumSlotsAvailable(null, [], MAGNETIC_MODULE_V2)
expect(result).toBe(1)
})

it('should return 2 for a thermocycler', () => {
const result = getNumSlotsAvailable(null, [], THERMOCYCLER_MODULE_V2)
expect(result).toBe(2)
})

it('should return 8 when there are no modules or additional equipment for a heater-shaker', () => {
const result = getNumSlotsAvailable(null, [], HEATERSHAKER_MODULE_V1)
expect(result).toBe(8)
})

it('should return 3 when there a plate reader', () => {
const mockModules = {
0: {
model: ABSORBANCE_READER_V1,
type: ABSORBANCE_READER_TYPE,
slot: 'B3',
},
}
const mockAdditionalEquipment: AdditionalEquipment[] = ['trashBin']
const result = getNumSlotsAvailable(
mockModules,
mockAdditionalEquipment,
'stagingArea'
)
// Note: the return value is 3 because trashBin can be placed slot1 and plate reader is on B3
expect(result).toBe(3)
})

it('should return 0 when there is a TC and 7 modules for a temperature module v2', () => {
const mockModules = {
0: {
@@ -90,6 +137,7 @@ describe('getNumSlotsAvailable', () => {
const result = getNumSlotsAvailable(mockModules, [], TEMPERATURE_MODULE_V2)
expect(result).toBe(0)
})

it('should return 1 when there are 9 additional equipment and 1 is a waste chute on the staging area and one is a gripper for a heater-shaker', () => {
const mockAdditionalEquipment: AdditionalEquipment[] = [
'trashBin',
@@ -109,6 +157,7 @@ describe('getNumSlotsAvailable', () => {
)
expect(result).toBe(1)
})

it('should return 1 when there is a full deck but one staging area for waste chute', () => {
const mockModules = {
0: {
@@ -148,6 +197,7 @@ describe('getNumSlotsAvailable', () => {
)
expect(result).toBe(1)
})

it('should return 1 when there are 7 modules (with one magnetic block) and one trash for staging area', () => {
const mockModules = {
0: {
@@ -187,8 +237,10 @@ describe('getNumSlotsAvailable', () => {
mockAdditionalEquipment,
'stagingArea'
)
expect(result).toBe(1)
// Note: the return value is 2 because trashBin can be placed slot1
expect(result).toBe(2)
})

it('should return 1 when there are 8 modules with 2 magnetic blocks and one trash for staging area', () => {
const mockModules = {
0: {
@@ -233,7 +285,7 @@ describe('getNumSlotsAvailable', () => {
mockAdditionalEquipment,
'stagingArea'
)
expect(result).toBe(1)
expect(result).toBe(2)
})
it('should return 4 when there are 12 magnetic blocks for staging area', () => {
const mockModules = {
Original file line number Diff line number Diff line change
@@ -127,7 +127,7 @@ export const DEFAULT_SLOT_MAP_FLEX: {
[HEATERSHAKER_MODULE_V1]: 'D1',
[MAGNETIC_BLOCK_V1]: 'D2',
[TEMPERATURE_MODULE_V2]: 'C1',
[ABSORBANCE_READER_V1]: 'D3',
[ABSORBANCE_READER_V1]: 'B3',
}

export const DEFAULT_SLOT_MAP_OT2: { [moduleType in ModuleType]?: string } = {
16 changes: 13 additions & 3 deletions protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom'
import {
FLEX_ROBOT_TYPE,
getAreSlotsAdjacent,
ABSORBANCE_READER_MODELS,
HEATERSHAKER_MODULE_TYPE,
MAGNETIC_BLOCK_TYPE,
MAGNETIC_MODULE_TYPE,
@@ -283,11 +284,20 @@ export function CreateNewProtocolWizard(): JSX.Element | null {
const stagingAreas = values.additionalEquipment.filter(
equipment => equipment === 'stagingArea'
)

if (stagingAreas.length > 0) {
// Note: when plate reader is present, cutoutB3 is not available for StagingArea
const hasPlateReader = modules.some(
module => module.model === ABSORBANCE_READER_MODELS[0]
)
stagingAreas.forEach((_, index) => {
return dispatch(
createDeckFixture('stagingArea', STAGING_AREA_CUTOUTS_ORDERED[index])
)
const stagingAreaCutout = hasPlateReader
? STAGING_AREA_CUTOUTS_ORDERED.filter(
cutout => cutout !== 'cutoutB3'
)[index]
: STAGING_AREA_CUTOUTS_ORDERED[index]

return dispatch(createDeckFixture('stagingArea', stagingAreaCutout))
})
}

65 changes: 39 additions & 26 deletions protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx
Original file line number Diff line number Diff line change
@@ -30,10 +30,11 @@ import type {
import type { DropdownOption } from '@opentrons/components'
import type { AdditionalEquipment, WizardFormState } from './types'

const TOTAL_OUTER_SLOTS = 8
const MIDDLE_SLOT_NUM = 4
const MAX_MAGNETIC_BLOCK_SLOTS = 12
const TOTAL_LEFT_SLOTS = 4
const NUM_SLOTS_OUTER = 8
const NUM_SLOTS_MIDDLE = 4
const NUM_SLOTS_COLUMN3 = 4
const NUM_SLOTS_MAGNETIC_BLOCK = 12

export const getNumOptions = (length: number): DropdownOption[] => {
return Array.from({ length }, (_, i) => ({
name: `${i + 1}`,
@@ -66,12 +67,12 @@ export const getNumSlotsAvailable = (
const magneticBlockCount = magneticBlocks.length
const moduleCount = modules != null ? Object.keys(modules).length : 0
let filteredModuleLength = moduleCount
if (magneticBlockCount <= MIDDLE_SLOT_NUM) {
if (magneticBlockCount <= NUM_SLOTS_MIDDLE) {
// Subtract magnetic blocks directly if their count is ≤ 4
filteredModuleLength -= magneticBlockCount
} else {
// Subtract the excess magnetic blocks beyond 4
const extraMagneticBlocks = magneticBlockCount - MIDDLE_SLOT_NUM
const extraMagneticBlocks = magneticBlockCount - NUM_SLOTS_MIDDLE
filteredModuleLength -= extraMagneticBlocks
}
if (hasTC) {
@@ -86,11 +87,9 @@ export const getNumSlotsAvailable = (
case 'gripper': {
return 0
}
// TODO: wire up absorbance reader
case ABSORBANCE_READER_V1: {
return 1
}

// these modules don't support MoaM
case ABSORBANCE_READER_V1:
case THERMOCYCLER_MODULE_V1:
case TEMPERATURE_MODULE_V1:
case MAGNETIC_MODULE_V1:
@@ -105,43 +104,45 @@ export const getNumSlotsAvailable = (
return 2
}
}

case 'trashBin':
case HEATERSHAKER_MODULE_V1:
case TEMPERATURE_MODULE_V2: {
return (
TOTAL_OUTER_SLOTS -
NUM_SLOTS_OUTER -
(filteredModuleLength + filteredAdditionalEquipmentLength)
)
}

case 'stagingArea': {
const lengthMinusMagneticBlock =
moduleCount + (hasTC ? 1 : 0) - magneticBlockCount
let adjustedModuleLength = 0
if (lengthMinusMagneticBlock > TOTAL_LEFT_SLOTS) {
adjustedModuleLength = lengthMinusMagneticBlock - TOTAL_LEFT_SLOTS
}

const occupiedSlots =
adjustedModuleLength + filteredAdditionalEquipmentLength

return TOTAL_LEFT_SLOTS - occupiedSlots
const modulesWithColumn3 =
modules !== null
? Object.values(modules).filter(module => module.slot?.includes('3'))
.length
: 0
const fixtureSlotsWithColumn3 =
additionalEquipment !== null
? additionalEquipment.filter(slot => slot.includes('3')).length
: 0
return NUM_SLOTS_COLUMN3 - modulesWithColumn3 - fixtureSlotsWithColumn3
}

case 'wasteChute': {
const adjustmentForStagingArea = numStagingAreas >= 1 ? 1 : 0
return (
TOTAL_OUTER_SLOTS -
NUM_SLOTS_OUTER -
(filteredModuleLength +
filteredAdditionalEquipmentLength -
adjustmentForStagingArea)
)
}

case MAGNETIC_BLOCK_V1: {
const filteredAdditionalEquipmentForMagneticBlockLength = additionalEquipment.filter(
ae => ae !== 'gripper' && ae !== 'stagingArea'
)?.length
return (
MAX_MAGNETIC_BLOCK_SLOTS -
NUM_SLOTS_MAGNETIC_BLOCK -
(filteredModuleLength +
filteredAdditionalEquipmentForMagneticBlockLength)
)
@@ -292,9 +293,21 @@ export const getTrashSlot = (values: WizardFormState): string => {
equipment.includes('stagingArea')
)

const cutouts = stagingAreas.map(
(_, index) => STAGING_AREA_CUTOUTS_ORDERED[index]
// when plate reader is present, cutoutB3 is not available for StagingArea
const hasPlateReader =
modules !== null
? Object.values(modules).some(
module => module.model === ABSORBANCE_READER_V1
)
: false
const cutouts = stagingAreas.map((_, index) =>
hasPlateReader
? STAGING_AREA_CUTOUTS_ORDERED.filter(cutout => cutout !== 'cutoutB3')[
index
]
: STAGING_AREA_CUTOUTS_ORDERED[index]
)

const hasWasteChute = additionalEquipment.find(equipment =>
equipment.includes('wasteChute')
)

0 comments on commit 8f8b055

Please sign in to comment.