Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(protocol-designer): update logic to add modules and fixtures with plate reader #17344

Merged
merged 4 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
WRAP,
} from '@opentrons/components'
import {
ABSORBANCE_READER_TYPE,
ABSORBANCE_READER_V1,
FLEX_ROBOT_TYPE,
getModuleDisplayName,
Expand Down Expand Up @@ -73,7 +72,6 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null {
TEMPERATURE_MODULE_TYPE,
HEATERSHAKER_MODULE_TYPE,
MAGNETIC_BLOCK_TYPE,
ABSORBANCE_READER_TYPE,
]
const hasGripper = additionalEquipment.some(aE => aE === 'gripper')

Expand Down Expand Up @@ -180,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
Expand Down Expand Up @@ -221,6 +220,9 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null {
gridGap={SPACING.spacing4}
>
{Object.entries(modules)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use .values() if we aren't using the properties here?

Copy link
Contributor Author

@koji koji Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not using .values() because of type errors.

.sort(([, moduleA], [, moduleB]) =>
moduleA.model.localeCompare(moduleB.model)
)
.reduce<Array<FormModule & { count: number; key: string }>>(
(acc, [key, module]) => {
const existingModule = acc.find(
Expand Down Expand Up @@ -255,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 (
Expand Down
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'
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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',
Expand All @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {
Expand Down
16 changes: 13 additions & 3 deletions protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
})
}

Expand Down
65 changes: 39 additions & 26 deletions protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down Expand Up @@ -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) {
Expand All @@ -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:
Expand All @@ -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)
)
Expand Down Expand Up @@ -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')
)
Expand Down
Loading