Skip to content

Commit

Permalink
feat(step-generation): implement logic for collision detection with 9…
Browse files Browse the repository at this point in the history
…6 partial pickup (#15725)

Closes RAUT-1046

This PR addresses a number of bugs to collision detection when using the
96-channel pipette with
single-column nozzle configuration. The main modifications lie in
`getIsSafePipetteMovement`, which
detects the location of the pipette relative to the target labware,
well, and offset, and confirms
whether any surrounding slots contain dangerous module + adapter +
labware combinations.
  • Loading branch information
ncdiehl11 authored Jul 22, 2024
1 parent c9a3f73 commit acedeb7
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 242 deletions.
16 changes: 16 additions & 0 deletions step-generation/src/__tests__/aspirate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe('aspirate', () => {
tipRack: 'tiprack1Id',
xOffset: 0,
yOffset: 0,
nozzles: null,
}
const result = aspirate(params, invariantContext, robotStateWithTip)
expect(getSuccessResult(result).commands).toEqual([
Expand Down Expand Up @@ -113,6 +114,7 @@ describe('aspirate', () => {
tipRack: 'tiprack1Id',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down Expand Up @@ -142,6 +144,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand All @@ -164,6 +167,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand All @@ -183,6 +187,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
initialRobotState
Expand All @@ -205,6 +210,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand All @@ -231,6 +237,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
initialRobotState
Expand Down Expand Up @@ -265,6 +272,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down Expand Up @@ -299,6 +307,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down Expand Up @@ -339,6 +348,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down Expand Up @@ -373,6 +383,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down Expand Up @@ -413,6 +424,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down Expand Up @@ -443,6 +455,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down Expand Up @@ -472,6 +485,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down Expand Up @@ -502,6 +516,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down Expand Up @@ -533,6 +548,7 @@ describe('aspirate', () => {
tipRack: 'tipRack',
xOffset: 0,
yOffset: 0,
nozzles: null,
},
invariantContext,
robotStateWithTip
Expand Down
4 changes: 4 additions & 0 deletions step-generation/src/__tests__/dispense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ describe('dispense', () => {
flowRate: 6,
xOffset: 0,
yOffset: 0,
tipRack: 'tiprack1Id',
nozzles: null,
}
})
it('dispense normally (with tip)', () => {
Expand Down Expand Up @@ -102,6 +104,8 @@ describe('dispense', () => {
well: 'A1',
xOffset: 0,
yOffset: 0,
tipRack: 'tiprack1Id',
nozzles: null,
},
invariantContext,
initialRobotState
Expand Down
119 changes: 68 additions & 51 deletions step-generation/src/__tests__/getIsSafePipetteMovement.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest'
import { expect, describe, it, beforeEach } from 'vitest'
import { getIsSafePipetteMovement } from '../utils'
import {
TEMPERATURE_MODULE_TYPE,
Expand All @@ -18,53 +18,62 @@ const mockTipUri = 'mockTipUri'
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 mockWellName = 'A1'

const mockRobotState: RobotState = {
pipettes: { pip: { mount: 'left' } },
labware: { [mockLabwareId]: { slot: 'D2' }, [mockTiprackId]: { slot: 'A2' } },
modules: {},
tipState: { tipracks: {}, pipettes: {} },
liquidState: { pipettes: {}, labware: {}, additionalEquipment: {} },
}
describe('getIsSafePipetteMovement', () => {
let mockInvariantProperties: InvariantContext
let mockRobotState: RobotState
beforeEach(() => {
mockInvariantProperties = {
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,
},
}
mockRobotState = {
pipettes: { pip: { mount: 'left' } },
labware: {
[mockLabwareId]: { slot: 'D2' },
[mockTiprackId]: { slot: 'A2' },
},
modules: {},
tipState: { tipracks: {}, pipettes: {} },
liquidState: { pipettes: {}, labware: {}, additionalEquipment: {} },
}
})

it('returns true when the labware id is a trash bin', () => {
const result = getIsSafePipetteMovement(
{
Expand Down Expand Up @@ -97,7 +106,8 @@ describe('getIsSafePipetteMovement', () => {
mockPipId,
mockLabwareId,
mockTipUri,
{ x: -12, y: -100, z: 20 }
{ x: -12, y: -100, z: 20 },
mockWellName
)
expect(result).toEqual(false)
})
Expand All @@ -118,25 +128,31 @@ describe('getIsSafePipetteMovement', () => {
mockPipId,
mockLabwareId,
mockTipUri,
{ x: -1, y: 5, z: 20 }
{ x: -1, y: 5, z: 20 },
mockWellName
)
expect(result).toEqual(true)
})
it('returns false when there is a tip that collides', () => {
mockRobotState.tipState.tipracks = { mockTiprackId: { A1: true } }
mockRobotState.labware = {
...mockRobotState.labware,
[mockAdapter]: { slot: 'D1' },
}
const result = getIsSafePipetteMovement(
mockRobotState,
mockInvariantProperties,
mockPipId,
mockLabwareId,
mockTipUri,
{ x: -1, y: 5, z: 0 }
{ x: -1, y: 5, z: 0 },
mockWellName
)
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 },
[mockModule]: { slot: 'D1', moduleState: {} as any },
}
mockRobotState.labware = {
[mockLabwareId]: { slot: 'D2' },
Expand All @@ -160,7 +176,8 @@ describe('getIsSafePipetteMovement', () => {
mockPipId,
mockLabwareId,
mockTipUri,
{ x: 0, y: 0, z: 0 }
{ x: 0, y: 0, z: 0 },
mockWellName
)
expect(result).toEqual(false)
})
Expand Down
29 changes: 27 additions & 2 deletions step-generation/src/commandCreators/atomic/aspirate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data'
import { COLUMN, FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data'
import * as errorCreators from '../../errorCreators'
import { getPipetteWithTipMaxVol } from '../../robotStateSelectors'
import {
Expand All @@ -12,16 +12,21 @@ import {
getIsHeaterShakerEastWestMultiChannelPipette,
getIsHeaterShakerNorthSouthOfNonTiprackWithMultiChannelPipette,
uuid,
getIsSafePipetteMovement,
} from '../../utils'
import { COLUMN_4_SLOTS } from '../../constants'
import type { CreateCommand } from '@opentrons/shared-data'
import type {
CreateCommand,
NozzleConfigurationStyle,
} from '@opentrons/shared-data'
import type { AspirateParams } from '@opentrons/shared-data/protocol/types/schemaV3'
import type { CommandCreator, CommandCreatorError } from '../../types'

export interface ExtendedAspirateParams extends AspirateParams {
xOffset: number
yOffset: number
tipRack: string
nozzles: NozzleConfigurationStyle | null
}
/** Aspirate with given args. Requires tip. */
export const aspirate: CommandCreator<ExtendedAspirateParams> = (
Expand All @@ -40,6 +45,7 @@ export const aspirate: CommandCreator<ExtendedAspirateParams> = (
tipRack,
xOffset,
yOffset,
nozzles,
} = args
const actionName = 'aspirate'
const labwareState = prevRobotState.labware
Expand Down Expand Up @@ -108,6 +114,25 @@ export const aspirate: CommandCreator<ExtendedAspirateParams> = (
)
}

const is96Channel =
invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96

if (
is96Channel &&
nozzles === COLUMN &&
!getIsSafePipetteMovement(
prevRobotState,
invariantContext,
args.pipette,
args.labware,
args.tipRack,
{ x: xOffset, y: yOffset, z: offsetFromBottomMm },
args.well
)
) {
errors.push(errorCreators.possiblePipetteCollision())
}

if (
thermocyclerPipetteCollision(
prevRobotState.modules,
Expand Down
Loading

0 comments on commit acedeb7

Please sign in to comment.