Skip to content

Commit

Permalink
feat(step-generation, shared-data): pipette collision warnings (#14989)
Browse files Browse the repository at this point in the history
closes AUTH-19
  • Loading branch information
jerader authored Apr 25, 2024
1 parent 04e00ad commit bd1f8da
Show file tree
Hide file tree
Showing 15 changed files with 768 additions and 388 deletions.
30 changes: 30 additions & 0 deletions shared-data/js/helpers/__tests__/getFlexSurroundingSlots.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest'
import { getFlexSurroundingSlots } from '../getFlexSurroundingSlots'

describe('getFlexSurroundingSlots', () => {
it('returns slots when slot is D2', () => {
const results = getFlexSurroundingSlots('D2', [])
expect(results).toStrictEqual(['C1', 'C2', 'C3', 'D1', 'D3'])
})
it('returns slots when selected is a center slot', () => {
const results = getFlexSurroundingSlots('C2', [])
expect(results).toStrictEqual([
'B1',
'B2',
'B3',
'C1',
'C3',
'D1',
'D2',
'D3',
])
})
it('returns slots when selected is a column 3 with staging areas present', () => {
const results = getFlexSurroundingSlots('B3', ['A4'])
expect(results).toStrictEqual(['A2', 'A3', 'A4', 'B2', 'C2', 'C3'])
})
it('returns slots when selected is a corner, A1', () => {
const results = getFlexSurroundingSlots('A1', ['A4'])
expect(results).toStrictEqual(['A2', 'B1', 'B2'])
})
})
63 changes: 63 additions & 0 deletions shared-data/js/helpers/getFlexSurroundingSlots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { DeckSlotId } from '../types'

const FLEX_GRID = [
['A1', 'A2', 'A3'],
['B1', 'B2', 'B3'],
['C1', 'C2', 'C3'],
['D1', 'D2', 'D3'],
]

const LETTER_TO_ROW_MAP: Record<string, number> = {
A: 0,
B: 1,
C: 2,
D: 3,
}

let COLS = 3 // Initial number of columns in each row
const ROWS = 4

const DIRECTIONS = [
[-1, -1], // NW
[-1, 0], // N
[-1, 1], // NE
[0, -1], // W
[0, 1], // E
[1, -1], // SW
[1, 0], // S
[1, 1], // SE
]

export const getFlexSurroundingSlots = (
slot: DeckSlotId,
stagingAreaSlots: DeckSlotId[]
): DeckSlotId[] => {
// Handle staging area slots
if (stagingAreaSlots.length > 0) {
stagingAreaSlots.forEach((stagingSlot, index) => {
if (stagingSlot) {
FLEX_GRID[index].push(stagingSlot)
}
})
COLS = Math.max(COLS, FLEX_GRID[0].length) // Update COLS to the maximum row length
}

const letter = slot.charAt(0)
const col = parseInt(slot.charAt(1)) - 1 // Convert the column to a 0-based index
const row = LETTER_TO_ROW_MAP[letter]

const surroundingSlots: DeckSlotId[] = []

// Iterate through both directions
DIRECTIONS.forEach(([dRow, dCol]) => {
const newRow = row + dRow
const newCol = col + dCol

if (newRow >= 0 && newRow < ROWS && newCol >= 0 && newCol < COLS) {
surroundingSlots.push(FLEX_GRID[newRow][newCol])
}
})

// Filter out any undefined values from the staging area slots that are not added
return surroundingSlots.filter(slot => slot !== undefined)
}
1 change: 1 addition & 0 deletions shared-data/js/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from './getLoadedLabwareDefinitionsByUri'
export * from './getOccludedSlotCountForModule'
export * from './labwareInference'
export * from './getAddressableAreasInProtocol'
export * from './getFlexSurroundingSlots'
export * from './getSimplestFlexDeckConfig'
export * from './formatRunTimeParameterDefaultValue'
export * from './formatRunTimeParameterValue'
Expand Down
167 changes: 167 additions & 0 deletions step-generation/src/__tests__/getIsSafePipetteMovement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { expect, describe, it } from 'vitest'
import { getIsSafePipetteMovement } from '../utils'
import {
LabwareDefinition2,
TEMPERATURE_MODULE_TYPE,
TEMPERATURE_MODULE_V2,
fixture96Plate,
fixtureP100096V2Specs,
fixtureTiprack1000ul,
fixtureTiprackAdapter,
} from '@opentrons/shared-data'
import { InvariantContext, RobotState } from '../types'

const mockLabwareId = 'labwareId'
const mockPipId = 'pip'
const mockTiprackId = 'tiprackId'
const mockModule = 'moduleId'
const mockLabware2 = 'labwareId2'
const mockAdapter = 'adapterId'
const mockInvariantProperties: InvariantContext = {
pipetteEntities: {
pip: {
name: 'p1000_96',
id: 'pip',
tiprackDefURI: ['mockDefUri'],
tiprackLabwareDef: [fixtureTiprack1000ul as LabwareDefinition2],
spec: fixtureP100096V2Specs,
},
},
labwareEntities: {
[mockLabwareId]: {
id: mockLabwareId,
labwareDefURI: 'mockDefUri',
def: fixture96Plate as LabwareDefinition2,
},
[mockTiprackId]: {
id: mockTiprackId,
labwareDefURI: 'mockTipUri',
def: fixtureTiprack1000ul as LabwareDefinition2,
},
[mockAdapter]: {
id: mockAdapter,
labwareDefURI: 'mockAdapterUri',
def: fixtureTiprackAdapter as LabwareDefinition2,
},
[mockLabware2]: {
id: mockLabware2,
labwareDefURI: 'mockDefUri',
def: fixture96Plate as LabwareDefinition2,
},
},
moduleEntities: {},
additionalEquipmentEntities: {},
config: {
OT_PD_DISABLE_MODULE_RESTRICTIONS: false,
},
}

const mockRobotState: RobotState = {
pipettes: { pip: { mount: 'left' } },
labware: { [mockLabwareId]: { slot: 'D2' }, [mockTiprackId]: { slot: 'A2' } },
modules: {},
tipState: { tipracks: {}, pipettes: {} },
liquidState: { pipettes: {}, labware: {}, additionalEquipment: {} },
}
describe('getIsSafePipetteMovement', () => {
it('returns true when the labware id is a trash bin', () => {
const result = getIsSafePipetteMovement(
{
labware: {},
pipettes: {},
modules: {},
tipState: {},
liquidState: {},
} as any,
{
labwareEntities: {},
pipetteEntities: {},
moduleEntities: {},
additionalEquipmentEntities: {
trashBin: { name: 'trashBin', location: 'A3', id: 'trashBin' },
},
config: {} as any,
},
'mockId',
'mockTrashBin',
'mockTiprackId',
{ x: 0, y: 0, z: 0 }
)
expect(result).toEqual(true)
})
it('returns false when within pipette extents is false', () => {
const result = getIsSafePipetteMovement(
mockRobotState,
mockInvariantProperties,
mockPipId,
mockLabwareId,
mockTiprackId,
{ x: -12, y: -100, z: 20 }
)
expect(result).toEqual(false)
})
it('returns true when there are no collisions and a module near it', () => {
mockRobotState.modules = {
[mockModule]: { slot: 'D1', moduleState: {} as any },
}
mockInvariantProperties.moduleEntities = {
[mockModule]: {
id: mockModule,
type: TEMPERATURE_MODULE_TYPE,
model: TEMPERATURE_MODULE_V2,
},
}
const result = getIsSafePipetteMovement(
mockRobotState,
mockInvariantProperties,
mockPipId,
mockLabwareId,
mockTiprackId,
{ x: -1, y: 5, z: 20 }
)
expect(result).toEqual(true)
})
it('returns false when there is a tip that collides', () => {
mockRobotState.tipState.tipracks = { mockTiprackId: { A1: true } }
const result = getIsSafePipetteMovement(
mockRobotState,
mockInvariantProperties,
mockPipId,
mockLabwareId,
mockTiprackId,
{ x: -1, y: 5, z: 0 }
)
expect(result).toEqual(false)
})
it('returns false when there is a tall module nearby in a diagonal slot with adapter and labware', () => {
mockRobotState.modules = {
[mockModule]: { slot: 'C1', moduleState: {} as any },
}
mockRobotState.labware = {
[mockLabwareId]: { slot: 'D2' },
[mockAdapter]: {
slot: mockModule,
},
[mockLabware2]: {
slot: mockAdapter,
},
}
mockInvariantProperties.moduleEntities = {
[mockModule]: {
id: mockModule,
type: TEMPERATURE_MODULE_TYPE,
model: TEMPERATURE_MODULE_V2,
},
}
const result = getIsSafePipetteMovement(
mockRobotState,
mockInvariantProperties,
mockPipId,
mockLabwareId,
mockTiprackId,
{ x: 0, y: 0, z: 0 }
)
expect(result).toEqual(false)
})
// todo(jr, 4/23/24): add more test cases, test thermocycler collision - i'll do this in a follow up
})
Loading

0 comments on commit bd1f8da

Please sign in to comment.