Skip to content

Commit bd1f8da

Browse files
authored
feat(step-generation, shared-data): pipette collision warnings (#14989)
closes AUTH-19
1 parent 04e00ad commit bd1f8da

File tree

15 files changed

+768
-388
lines changed

15 files changed

+768
-388
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { getFlexSurroundingSlots } from '../getFlexSurroundingSlots'
3+
4+
describe('getFlexSurroundingSlots', () => {
5+
it('returns slots when slot is D2', () => {
6+
const results = getFlexSurroundingSlots('D2', [])
7+
expect(results).toStrictEqual(['C1', 'C2', 'C3', 'D1', 'D3'])
8+
})
9+
it('returns slots when selected is a center slot', () => {
10+
const results = getFlexSurroundingSlots('C2', [])
11+
expect(results).toStrictEqual([
12+
'B1',
13+
'B2',
14+
'B3',
15+
'C1',
16+
'C3',
17+
'D1',
18+
'D2',
19+
'D3',
20+
])
21+
})
22+
it('returns slots when selected is a column 3 with staging areas present', () => {
23+
const results = getFlexSurroundingSlots('B3', ['A4'])
24+
expect(results).toStrictEqual(['A2', 'A3', 'A4', 'B2', 'C2', 'C3'])
25+
})
26+
it('returns slots when selected is a corner, A1', () => {
27+
const results = getFlexSurroundingSlots('A1', ['A4'])
28+
expect(results).toStrictEqual(['A2', 'B1', 'B2'])
29+
})
30+
})
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { DeckSlotId } from '../types'
2+
3+
const FLEX_GRID = [
4+
['A1', 'A2', 'A3'],
5+
['B1', 'B2', 'B3'],
6+
['C1', 'C2', 'C3'],
7+
['D1', 'D2', 'D3'],
8+
]
9+
10+
const LETTER_TO_ROW_MAP: Record<string, number> = {
11+
A: 0,
12+
B: 1,
13+
C: 2,
14+
D: 3,
15+
}
16+
17+
let COLS = 3 // Initial number of columns in each row
18+
const ROWS = 4
19+
20+
const DIRECTIONS = [
21+
[-1, -1], // NW
22+
[-1, 0], // N
23+
[-1, 1], // NE
24+
[0, -1], // W
25+
[0, 1], // E
26+
[1, -1], // SW
27+
[1, 0], // S
28+
[1, 1], // SE
29+
]
30+
31+
export const getFlexSurroundingSlots = (
32+
slot: DeckSlotId,
33+
stagingAreaSlots: DeckSlotId[]
34+
): DeckSlotId[] => {
35+
// Handle staging area slots
36+
if (stagingAreaSlots.length > 0) {
37+
stagingAreaSlots.forEach((stagingSlot, index) => {
38+
if (stagingSlot) {
39+
FLEX_GRID[index].push(stagingSlot)
40+
}
41+
})
42+
COLS = Math.max(COLS, FLEX_GRID[0].length) // Update COLS to the maximum row length
43+
}
44+
45+
const letter = slot.charAt(0)
46+
const col = parseInt(slot.charAt(1)) - 1 // Convert the column to a 0-based index
47+
const row = LETTER_TO_ROW_MAP[letter]
48+
49+
const surroundingSlots: DeckSlotId[] = []
50+
51+
// Iterate through both directions
52+
DIRECTIONS.forEach(([dRow, dCol]) => {
53+
const newRow = row + dRow
54+
const newCol = col + dCol
55+
56+
if (newRow >= 0 && newRow < ROWS && newCol >= 0 && newCol < COLS) {
57+
surroundingSlots.push(FLEX_GRID[newRow][newCol])
58+
}
59+
})
60+
61+
// Filter out any undefined values from the staging area slots that are not added
62+
return surroundingSlots.filter(slot => slot !== undefined)
63+
}

shared-data/js/helpers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export * from './getLoadedLabwareDefinitionsByUri'
2727
export * from './getOccludedSlotCountForModule'
2828
export * from './labwareInference'
2929
export * from './getAddressableAreasInProtocol'
30+
export * from './getFlexSurroundingSlots'
3031
export * from './getSimplestFlexDeckConfig'
3132
export * from './formatRunTimeParameterDefaultValue'
3233
export * from './formatRunTimeParameterValue'
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { expect, describe, it } from 'vitest'
2+
import { getIsSafePipetteMovement } from '../utils'
3+
import {
4+
LabwareDefinition2,
5+
TEMPERATURE_MODULE_TYPE,
6+
TEMPERATURE_MODULE_V2,
7+
fixture96Plate,
8+
fixtureP100096V2Specs,
9+
fixtureTiprack1000ul,
10+
fixtureTiprackAdapter,
11+
} from '@opentrons/shared-data'
12+
import { InvariantContext, RobotState } from '../types'
13+
14+
const mockLabwareId = 'labwareId'
15+
const mockPipId = 'pip'
16+
const mockTiprackId = 'tiprackId'
17+
const mockModule = 'moduleId'
18+
const mockLabware2 = 'labwareId2'
19+
const mockAdapter = 'adapterId'
20+
const mockInvariantProperties: InvariantContext = {
21+
pipetteEntities: {
22+
pip: {
23+
name: 'p1000_96',
24+
id: 'pip',
25+
tiprackDefURI: ['mockDefUri'],
26+
tiprackLabwareDef: [fixtureTiprack1000ul as LabwareDefinition2],
27+
spec: fixtureP100096V2Specs,
28+
},
29+
},
30+
labwareEntities: {
31+
[mockLabwareId]: {
32+
id: mockLabwareId,
33+
labwareDefURI: 'mockDefUri',
34+
def: fixture96Plate as LabwareDefinition2,
35+
},
36+
[mockTiprackId]: {
37+
id: mockTiprackId,
38+
labwareDefURI: 'mockTipUri',
39+
def: fixtureTiprack1000ul as LabwareDefinition2,
40+
},
41+
[mockAdapter]: {
42+
id: mockAdapter,
43+
labwareDefURI: 'mockAdapterUri',
44+
def: fixtureTiprackAdapter as LabwareDefinition2,
45+
},
46+
[mockLabware2]: {
47+
id: mockLabware2,
48+
labwareDefURI: 'mockDefUri',
49+
def: fixture96Plate as LabwareDefinition2,
50+
},
51+
},
52+
moduleEntities: {},
53+
additionalEquipmentEntities: {},
54+
config: {
55+
OT_PD_DISABLE_MODULE_RESTRICTIONS: false,
56+
},
57+
}
58+
59+
const mockRobotState: RobotState = {
60+
pipettes: { pip: { mount: 'left' } },
61+
labware: { [mockLabwareId]: { slot: 'D2' }, [mockTiprackId]: { slot: 'A2' } },
62+
modules: {},
63+
tipState: { tipracks: {}, pipettes: {} },
64+
liquidState: { pipettes: {}, labware: {}, additionalEquipment: {} },
65+
}
66+
describe('getIsSafePipetteMovement', () => {
67+
it('returns true when the labware id is a trash bin', () => {
68+
const result = getIsSafePipetteMovement(
69+
{
70+
labware: {},
71+
pipettes: {},
72+
modules: {},
73+
tipState: {},
74+
liquidState: {},
75+
} as any,
76+
{
77+
labwareEntities: {},
78+
pipetteEntities: {},
79+
moduleEntities: {},
80+
additionalEquipmentEntities: {
81+
trashBin: { name: 'trashBin', location: 'A3', id: 'trashBin' },
82+
},
83+
config: {} as any,
84+
},
85+
'mockId',
86+
'mockTrashBin',
87+
'mockTiprackId',
88+
{ x: 0, y: 0, z: 0 }
89+
)
90+
expect(result).toEqual(true)
91+
})
92+
it('returns false when within pipette extents is false', () => {
93+
const result = getIsSafePipetteMovement(
94+
mockRobotState,
95+
mockInvariantProperties,
96+
mockPipId,
97+
mockLabwareId,
98+
mockTiprackId,
99+
{ x: -12, y: -100, z: 20 }
100+
)
101+
expect(result).toEqual(false)
102+
})
103+
it('returns true when there are no collisions and a module near it', () => {
104+
mockRobotState.modules = {
105+
[mockModule]: { slot: 'D1', moduleState: {} as any },
106+
}
107+
mockInvariantProperties.moduleEntities = {
108+
[mockModule]: {
109+
id: mockModule,
110+
type: TEMPERATURE_MODULE_TYPE,
111+
model: TEMPERATURE_MODULE_V2,
112+
},
113+
}
114+
const result = getIsSafePipetteMovement(
115+
mockRobotState,
116+
mockInvariantProperties,
117+
mockPipId,
118+
mockLabwareId,
119+
mockTiprackId,
120+
{ x: -1, y: 5, z: 20 }
121+
)
122+
expect(result).toEqual(true)
123+
})
124+
it('returns false when there is a tip that collides', () => {
125+
mockRobotState.tipState.tipracks = { mockTiprackId: { A1: true } }
126+
const result = getIsSafePipetteMovement(
127+
mockRobotState,
128+
mockInvariantProperties,
129+
mockPipId,
130+
mockLabwareId,
131+
mockTiprackId,
132+
{ x: -1, y: 5, z: 0 }
133+
)
134+
expect(result).toEqual(false)
135+
})
136+
it('returns false when there is a tall module nearby in a diagonal slot with adapter and labware', () => {
137+
mockRobotState.modules = {
138+
[mockModule]: { slot: 'C1', moduleState: {} as any },
139+
}
140+
mockRobotState.labware = {
141+
[mockLabwareId]: { slot: 'D2' },
142+
[mockAdapter]: {
143+
slot: mockModule,
144+
},
145+
[mockLabware2]: {
146+
slot: mockAdapter,
147+
},
148+
}
149+
mockInvariantProperties.moduleEntities = {
150+
[mockModule]: {
151+
id: mockModule,
152+
type: TEMPERATURE_MODULE_TYPE,
153+
model: TEMPERATURE_MODULE_V2,
154+
},
155+
}
156+
const result = getIsSafePipetteMovement(
157+
mockRobotState,
158+
mockInvariantProperties,
159+
mockPipId,
160+
mockLabwareId,
161+
mockTiprackId,
162+
{ x: 0, y: 0, z: 0 }
163+
)
164+
expect(result).toEqual(false)
165+
})
166+
// todo(jr, 4/23/24): add more test cases, test thermocycler collision - i'll do this in a follow up
167+
})

0 commit comments

Comments
 (0)