Skip to content

Commit fc09053

Browse files
authored
feat(protocol-designer): add logic for lid move compatibility (#19058)
This PR extends functionality for moving lids around the deck according to product specs. As part of this work, I extend `LabwareTemporalProperties` in `step-generation` and `protocol-designer` to include a new optional property `isUsedLid`. This property flips to `true` if the lid is loaded on or moved to a "pipettable" labware, which could presumably contain liquid, at any point. We check this property's value when determining safe moves to labware stacks. If the lid has been used, we do not allow its further movement to a new pipettable labware for sterility concerns. Closes AUTH-1071
1 parent 2d58c37 commit fc09053

File tree

11 files changed

+287
-83
lines changed

11 files changed

+287
-83
lines changed

protocol-designer/src/file-data/selectors/commands.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import omit from 'lodash/omit'
44
import uniqBy from 'lodash/uniqBy'
55
import { createSelector } from 'reselect'
66

7+
import { getIsLid, getIsPipettableLabware } from '@opentrons/shared-data'
78
import * as StepGeneration from '@opentrons/step-generation'
9+
import {
10+
getNearestParentInStack,
11+
TOUCHED_PIPETTABLE_LABWARE,
12+
} from '@opentrons/step-generation'
813

914
import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
1015
import { selectors as stepFormSelectors } from '../../step-forms'
1116

1217
import type { StepIdType } from '../../form-types'
1318
import type {
14-
LabwareOnDeck,
1519
LabwareTemporalProperties,
1620
ModuleOnDeck,
1721
ModuleTemporalProperties,
@@ -67,9 +71,24 @@ export const getInitialRobotState: (
6771
)
6872
const labware: Record<string, LabwareTemporalProperties> = mapValues(
6973
initialDeckSetup.labware,
70-
(l: LabwareOnDeck): LabwareTemporalProperties => ({
71-
stack: l.stack,
72-
})
74+
({ id, stack }): LabwareTemporalProperties => {
75+
const labwareEntity = invariantContext.labwareEntities[id]
76+
const isLid = getIsLid(labwareEntity.def)
77+
const nearestParent = getNearestParentInStack(stack)
78+
const isParentPipettableLabware =
79+
nearestParent != null &&
80+
nearestParent in invariantContext.labwareEntities &&
81+
getIsPipettableLabware(
82+
invariantContext.labwareEntities[nearestParent].def
83+
)
84+
return {
85+
stack,
86+
// set sterility to TOUCHED_PIPETTABLE_LABWARE if the labware is a lid and the parent is pipettable labware
87+
...(isLid && isParentPipettableLabware
88+
? { sterility: TOUCHED_PIPETTABLE_LABWARE }
89+
: {}),
90+
}
91+
}
7392
)
7493
const modules: Record<string, ModuleTemporalProperties> = mapValues(
7594
initialDeckSetup.modules,

protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLabwareTools/LabwareLocationField.tsx

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { useTranslation } from 'react-i18next'
22
import { useDispatch, useSelector } from 'react-redux'
33

4-
import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data'
54
import { getSlotInLocationStack } from '@opentrons/step-generation'
65

76
import { DropdownStepFormField } from '../../../../../../components/molecules'
87
import { getEnableStacking } from '../../../../../../feature-flags/selectors'
9-
import { getAdditionalEquipmentEntities } from '../../../../../../step-forms/selectors'
8+
import { getLabwareEntities } from '../../../../../../step-forms/selectors'
109
import {
1110
getDeckSetupForActiveItem,
1211
getRobotStateAtActiveItem,
@@ -29,15 +28,19 @@ export function LabwareLocationField(
2928
const { t } = useTranslation(['form', 'protocol_steps'])
3029
const { labware, useGripper } = props
3130
const enableStacking = useSelector(getEnableStacking)
32-
const additionalEquipmentEntities = useSelector(
33-
getAdditionalEquipmentEntities
34-
)
3531
const { labware: deckSetupLabware } = useSelector(getDeckSetupForActiveItem)
3632
const dispatch = useDispatch()
33+
const labwareEntities = useSelector(getLabwareEntities)
3734
const robotState = useSelector(getRobotStateAtActiveItem)
3835
const unoccupiedLabwareStackOptions: Option[] =
3936
robotState && enableStacking
40-
? getUnoccupiedStackOptions(robotState, deckSetupLabware, labware, t)
37+
? getUnoccupiedStackOptions({
38+
robotState,
39+
deckSetupLabware,
40+
labwareIdFromDropdown: labware,
41+
labwareEntities,
42+
t,
43+
})
4144
: []
4245
const isLabwareOffDeck =
4346
labware != null
@@ -48,26 +51,14 @@ export function LabwareLocationField(
4851
const unoccupiedLabwareLocationsOptionsSelector =
4952
useSelector(getUnoccupiedLabwareLocationOptions) ?? []
5053

51-
let unoccupiedLabwareLocationsOptions = [
54+
// invalid offDeck move filter
55+
const unoccupiedLabwareLocationsOptions = [
5256
...unoccupiedLabwareStackOptions,
5357
...unoccupiedLabwareLocationsOptionsSelector,
54-
]
55-
if (useGripper || isLabwareOffDeck) {
56-
unoccupiedLabwareLocationsOptions = unoccupiedLabwareLocationsOptions.filter(
57-
option => option.value !== 'offDeck'
58-
)
59-
}
60-
61-
if (
62-
!useGripper &&
63-
Object.values(additionalEquipmentEntities).find(
64-
ae => ae.name === 'wasteChute'
65-
) != null
66-
) {
67-
unoccupiedLabwareLocationsOptions = unoccupiedLabwareLocationsOptions.filter(
68-
option => option.value !== WASTE_CHUTE_CUTOUT
69-
)
70-
}
58+
].filter(option => {
59+
const canMoveOffDeck = !(useGripper || isLabwareOffDeck)
60+
return option.value !== 'offDeck' || canMoveOffDeck
61+
})
7162

7263
return (
7364
<DropdownStepFormField

protocol-designer/src/pages/Designer/__tests__/utils.test.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,13 @@ describe('getUnoccupiedStackOptions', () => {
290290
labId3: mockLabOnStagingArea,
291291
}
292292
expect(
293-
getUnoccupiedStackOptions(mockRobotState, mockLabware, 'labId2', mockT)
293+
getUnoccupiedStackOptions({
294+
robotState: mockRobotState,
295+
deckSetupLabware: mockLabware,
296+
labwareIdFromDropdown: 'labId2',
297+
labwareEntities: mockLabware,
298+
t: mockT,
299+
})
294300
).toEqual([
295301
{
296302
name: 'Fixture Flex 96 Tip Rack Adapter',
@@ -304,7 +310,32 @@ describe('getUnoccupiedStackOptions', () => {
304310
labId: mockLabOnDeck1Flex,
305311
}
306312
expect(
307-
getUnoccupiedStackOptions(mockRobotState, mockLabware, 'labId', mockT)
313+
getUnoccupiedStackOptions({
314+
robotState: mockRobotState,
315+
deckSetupLabware: mockLabware,
316+
labwareIdFromDropdown: 'labId',
317+
labwareEntities: mockLabware,
318+
t: mockT,
319+
})
320+
).toEqual([])
321+
})
322+
323+
it('should filter out labware that was moved to a waste chute', () => {
324+
const mockLabware: AllTemporalPropertiesForTimelineFrame['labware'] = {
325+
labId: mockLabOnDeck1Flex,
326+
labId2: {
327+
...mockLabOnDeck2Flex,
328+
stack: ['labId2', 'gripperWasteChute'],
329+
},
330+
}
331+
expect(
332+
getUnoccupiedStackOptions({
333+
robotState: mockRobotState,
334+
deckSetupLabware: mockLabware,
335+
labwareIdFromDropdown: 'labId',
336+
labwareEntities: mockLabware,
337+
t: mockT,
338+
})
308339
).toEqual([])
309340
})
310341
})

protocol-designer/src/pages/Designer/utils.ts

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { reduce } from 'lodash'
55
import {
66
FLEX_ROBOT_TYPE,
77
getAllLabwareDefs,
8+
getIsPipettableLabware,
89
getIsTiprack,
910
getPositionFromSlotId,
1011
TC_MODULE_LOCATION_OT2,
@@ -13,7 +14,9 @@ import {
1314
} from '@opentrons/shared-data'
1415
import {
1516
getFullStackFromLabwares,
17+
getNearestParentInStack,
1618
getSlotInLocationStack,
19+
TOUCHED_PIPETTABLE_LABWARE,
1720
} from '@opentrons/step-generation'
1821

1922
import { getRobotType } from '../../file-data/selectors'
@@ -36,6 +39,7 @@ import type {
3639
import type {
3740
AdditionalEquipmentName,
3841
DeckSlot,
42+
LabwareEntities,
3943
LabwareEntity,
4044
RobotState,
4145
} from '@opentrons/step-generation'
@@ -365,23 +369,37 @@ export const useLabwareDropdownOptions = (
365369
}
366370

367371
// used for LabwareLocationField dropdown
368-
export const getUnoccupiedStackOptions = (
369-
robotState: RobotState,
370-
deckSetupLabware: AllTemporalPropertiesForTimelineFrame['labware'],
371-
labwareIdFromDropdown: string,
372+
export const getUnoccupiedStackOptions = (args: {
373+
robotState: RobotState
374+
deckSetupLabware: AllTemporalPropertiesForTimelineFrame['labware']
375+
labwareIdFromDropdown: string
376+
labwareEntities: LabwareEntities
372377
t: any
373-
): Option[] => {
378+
}): Option[] => {
379+
const {
380+
robotState,
381+
deckSetupLabware,
382+
labwareIdFromDropdown,
383+
labwareEntities,
384+
t,
385+
} = args
374386
if (deckSetupLabware[labwareIdFromDropdown] == null) {
375387
return []
376388
}
377389

378390
const { def } = deckSetupLabware[labwareIdFromDropdown]
379391
const labwareCompatibleParentLabware = def.compatibleParentLabware
380392

381-
return Object.entries(robotState.labware).reduce<Option[]>(
393+
const { labware: labwareState } = robotState
394+
395+
const isLabwareToMoveUsedLid =
396+
labwareState[labwareIdFromDropdown]?.sterility ===
397+
TOUCHED_PIPETTABLE_LABWARE
398+
399+
return Object.entries(labwareState).reduce<Option[]>(
382400
(acc, [labwareId, temporalLabwareOnDeck]) => {
383401
const slot = getSlotInLocationStack(temporalLabwareOnDeck.stack)
384-
const fullStack = getFullStackFromLabwares(robotState.labware, slot)
402+
const fullStack = getFullStackFromLabwares(labwareState, slot)
385403
const labwareOnDeck = deckSetupLabware[labwareId]
386404
const isTopOfStack = fullStack[0] === labwareId
387405
const { def: labwareOnDeckDef } = labwareOnDeck
@@ -393,23 +411,45 @@ export const getUnoccupiedStackOptions = (
393411
labwareIdFromDropdown
394412
)
395413

396-
if (isTopOfStack && isCompatible && isNotCurrentLabwareStack) {
414+
const nearestParentInStack = getNearestParentInStack(fullStack)
415+
const isNearestParentPipettableLabware =
416+
nearestParentInStack != null &&
417+
labwareEntities[nearestParentInStack] != null &&
418+
getIsPipettableLabware(labwareEntities[nearestParentInStack].def)
419+
420+
const isNewLabwarePipettable =
421+
labwareOnDeckDef != null && getIsPipettableLabware(labwareOnDeckDef)
422+
const isSafeLidMove =
423+
!(isLabwareToMoveUsedLid && isNewLabwarePipettable) &&
424+
!isNearestParentPipettableLabware
425+
426+
const isInWasteChute = slot === 'gripperWasteChute'
427+
428+
if (
429+
isTopOfStack &&
430+
isCompatible &&
431+
isNotCurrentLabwareStack &&
432+
!isInWasteChute &&
433+
isSafeLidMove
434+
) {
397435
const similarLabwareStackIds = getAllLabwareIdsOfCertainURIOnStack(
398436
deckSetupLabware,
399437
labwareOnDeck
400438
)
401-
acc.push({
402-
name:
403-
similarLabwareStackIds.length > 1
404-
? t('protocol_steps:unoccupied_stack', {
405-
name: displayName,
406-
})
407-
: displayName,
408-
value: labwareId,
409-
deckLabel: slot,
410-
})
439+
return [
440+
...acc,
441+
{
442+
name:
443+
similarLabwareStackIds.length > 1
444+
? t('protocol_steps:unoccupied_stack', {
445+
name: displayName,
446+
})
447+
: displayName,
448+
value: labwareId,
449+
deckLabel: slot,
450+
},
451+
]
411452
}
412-
413453
return acc
414454
},
415455
[]

protocol-designer/src/step-forms/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
ModuleEntity,
1919
PipetteEntity,
2020
TemperatureStatus,
21+
TOUCHED_PIPETTABLE_LABWARE,
2122
} from '@opentrons/step-generation'
2223
import type { DeckSlot } from '../types'
2324

@@ -107,6 +108,9 @@ export type NormalizedLabware = NormalizedLabwareById[keyof NormalizedLabwareByI
107108
// Temporal properties (eg location) that are time-variant
108109
export interface LabwareTemporalProperties {
109110
stack: string[] // a stack of ids from top to bottom
111+
// we currently use this property only to track if a lid has been placed on a "pipettable" labware that could presumably contain liquid
112+
// we can expand this type in the future to track other types of sterility for various labware types
113+
sterility?: typeof TOUCHED_PIPETTABLE_LABWARE
110114
}
111115
export interface PipetteTemporalProperties {
112116
mount: Mount

protocol-designer/src/top-selectors/labware-locations/index.ts

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -269,24 +269,23 @@ export const getUnoccupiedLabwareLocationOptions: Selector<
269269
)
270270
)
271271

272-
const unoccupiedSlotOptions = allSlotIds
273-
.filter(slotId => {
274-
const isTrashSlot =
275-
robotType === FLEX_ROBOT_TYPE
276-
? MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(slotId)
277-
: ['fixedTrash', '12'].includes(slotId)
278-
return (
279-
!slotIdsOccupiedByModules.includes(slotId) &&
280-
!Object.values(labware).some(lw => lw.stack.includes(slotId)) &&
281-
!isTrashSlot &&
282-
!trashCutouts.some(cutout => cutout.includes(slotId)) &&
283-
!WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slotId) &&
284-
!notSelectedStagingAreaAddressableAreas.includes(slotId) &&
285-
!FLEX_MODULE_ADDRESSABLE_AREAS.includes(slotId) &&
286-
!FLEX_STACKER_ADDRESSABLE_AREAS.includes(slotId)
287-
)
288-
})
289-
.map(slotId => ({ name: slotId, value: slotId, deckLabel: slotId }))
272+
const unoccupiedSlotOptions = allSlotIds.reduce<Option[]>((acc, slotId) => {
273+
const isTrashSlot =
274+
robotType === FLEX_ROBOT_TYPE
275+
? MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(slotId)
276+
: ['fixedTrash', '12'].includes(slotId)
277+
return !slotIdsOccupiedByModules.includes(slotId) &&
278+
!Object.values(labware).some(lw => lw.stack.includes(slotId)) &&
279+
!isTrashSlot &&
280+
!trashCutouts.some(cutout => cutout.includes(slotId)) &&
281+
!WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slotId) &&
282+
!notSelectedStagingAreaAddressableAreas.includes(slotId) &&
283+
!FLEX_MODULE_ADDRESSABLE_AREAS.includes(slotId) &&
284+
!FLEX_STACKER_ADDRESSABLE_AREAS.includes(slotId)
285+
? [...acc, { name: slotId, value: slotId, deckLabel: slotId }]
286+
: acc
287+
}, [])
288+
290289
const offDeck = {
291290
name: 'Off-deck',
292291
value: OFFDECK,
@@ -298,20 +297,13 @@ export const getUnoccupiedLabwareLocationOptions: Selector<
298297
deckLabel: 'D3',
299298
}
300299

301-
return hasWasteChute
302-
? [
303-
wasteChuteSlot,
304-
...unoccupiedAdapterOptions,
305-
...unoccupiedModuleOptions,
306-
...unoccupiedSlotOptions,
307-
offDeck,
308-
]
309-
: [
310-
...unoccupiedAdapterOptions,
311-
...unoccupiedModuleOptions,
312-
...unoccupiedSlotOptions,
313-
offDeck,
314-
]
300+
return [
301+
...(hasWasteChute ? [wasteChuteSlot] : []),
302+
...unoccupiedAdapterOptions,
303+
...unoccupiedModuleOptions,
304+
...unoccupiedSlotOptions,
305+
offDeck,
306+
]
315307
}
316308
)
317309

0 commit comments

Comments
 (0)