Skip to content

Commit 6dafd64

Browse files
committed
fix(step-generation,-shared-data): wire up deck extent checks in safePipetteMovemnts
This PR wires up deck extent checks in `safePipetteMovements.ts` in step generation. In short, we need to check not only on-deck collisions with partial tip configuration pipette movements, but also absolute pipette target positions relative to the limits of the gantry. Closes https://opentrons.atlassian.net/browse/RQA-4969
1 parent a64e838 commit 6dafd64

File tree

4 files changed

+94
-35
lines changed

4 files changed

+94
-35
lines changed

shared-data/js/helpers/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import uniq from 'lodash/uniq'
22

33
import standardOt2DeckDef from '../../deck/definitions/5/ot2_standard.json'
44
import standardFlexDeckDef from '../../deck/definitions/5/ot3_standard.json'
5-
import { OPENTRONS_LABWARE_NAMESPACE } from '../constants'
5+
import standardOt2RobotDef from '../../robot/definitions/1/ot2.json'
6+
import standardFlexRobotDef from '../../robot/definitions/1/ot3.json'
7+
import { FLEX_ROBOT_TYPE, OPENTRONS_LABWARE_NAMESPACE } from '../constants'
68
import { getAllLiquidClassDefs } from '../liquidClasses'
79
import { getSchema2Dimensions } from './positionMath'
810

@@ -12,6 +14,7 @@ import type {
1214
LabwareDefinition,
1315
LiquidClass,
1416
ModuleModel,
17+
RobotDefinition,
1518
RobotType,
1619
ThermalAdapterName,
1720
} from '../types'
@@ -465,3 +468,11 @@ export const getSortedLiquidClassDefs = (): Record<string, LiquidClass> => {
465468
)
466469
)
467470
}
471+
472+
export const getRobotDefFromRobotType = (
473+
robotType: RobotType
474+
): RobotDefinition => {
475+
return (
476+
robotType === FLEX_ROBOT_TYPE ? standardFlexRobotDef : standardOt2RobotDef
477+
) as RobotDefinition
478+
}

shared-data/js/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ export interface RobotDefinition {
4949
displayName: string
5050
robotType: RobotType
5151
models: string[]
52+
extents: CoordinateTuple
53+
paddingOffsets: {
54+
rear: number
55+
front: number
56+
leftSide: number
57+
rightSide: number
58+
}
59+
mountOffsets: {
60+
left: CoordinateTuple
61+
right: CoordinateTuple
62+
gripper?: CoordinateTuple
63+
}
5264
}
5365

5466
// TODO Ian 2019-06-04 split this out into eg ../labware/flowTypes/labwareV1.js

shared-data/tsconfig-data.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"pipette/**/*.json",
2121
"protocol/**/*.json",
2222
"errors/**/*.json",
23-
"build/**/*.json"
23+
"build/**/*.json",
24+
"robot/**/*.json"
2425
],
2526
"exclude": ["**/*.ts"]
2627
}

step-generation/src/utils/safePipetteMovements.ts

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getModuleDef,
99
getOt2SurroundingSlots,
1010
getPositionFromSlotId,
11+
getRobotDefFromRobotType,
1112
OT2_ROBOT_TYPE,
1213
SINGLE,
1314
THERMOCYCLER_MODULE_TYPE,
@@ -36,8 +37,6 @@ import type {
3637
TipState,
3738
} from '../types'
3839

39-
const A12_column_front_left_bound = { x: -11.03, y: 2 }
40-
const A12_column_back_right_bound = { x: 526.77, y: 506.2 }
4140
export const PRIMARY_NOZZLE = 'A12'
4241
const FLEX_TC_LID_COLLISION_ZONE = {
4342
back_left: { x: -43.25, y: 454.9, z: 211.91 },
@@ -65,26 +64,6 @@ export interface Point {
6564
z?: number
6665
}
6766

68-
// check if nozzle(s) are inbounds
69-
const getIsWithinPipetteExtents = (
70-
location: Point,
71-
nozzleConfiguration: NozzleConfigurationStyle,
72-
primaryNozzle: string
73-
): boolean => {
74-
if (nozzleConfiguration === 'COLUMN' && primaryNozzle === 'A12') {
75-
const isWithinBounds =
76-
A12_column_front_left_bound.x <= location.x &&
77-
location.x <= A12_column_back_right_bound.x &&
78-
A12_column_front_left_bound.y <= location.y &&
79-
location.y <= A12_column_back_right_bound.y
80-
81-
return isWithinBounds
82-
} else {
83-
// TODO: Handle other configurations such as 8-channel partial tip, and eventually all pipettes.
84-
return true
85-
}
86-
}
87-
8867
// return pipette bounds at a sepcific position
8968
// note that this calculation is pessimistic to mirror behavior on protocol engine
9069
// the returned plane is defined by the z-height of the empty nozzles (lowest case scenario)
@@ -287,7 +266,7 @@ const getWellPosition = (
287266
labwareEntity: LabwareEntity,
288267
wellName: string,
289268
wellLocationOffset: Point,
290-
addressableAreaOffset: CoordinateTuple | null,
269+
addressableAreaOffset: CoordinateTuple,
291270
hasTip: boolean
292271
): Point => {
293272
const { wells } = labwareEntity.def
@@ -368,12 +347,27 @@ export const getIsSafePipetteMovement = (args: {
368347
const addressableAreaOffset = getPositionFromSlotId(
369348
labwareSlot,
370349
deckDefinition
371-
)
350+
) ?? [0, 0, 0]
351+
const isOnFlexThermocycler =
352+
robotType === FLEX_ROBOT_TYPE &&
353+
labwareState[labwareId].stack.some(
354+
item => moduleEntities[item]?.type === THERMOCYCLER_MODULE_TYPE
355+
)
356+
const thermocyclerOffset = isOnFlexThermocycler
357+
? (deckDefinition.locations.addressableAreas.find(
358+
addressableArea => addressableArea.id === 'thermocyclerModuleV2'
359+
)?.offsetFromCutoutFixture ?? [0, 0, 0])
360+
: [0, 0, 0]
361+
const fullOffset = [
362+
thermocyclerOffset[0] + addressableAreaOffset[0],
363+
thermocyclerOffset[1] + addressableAreaOffset[1],
364+
thermocyclerOffset[2] + addressableAreaOffset[2],
365+
]
372366
const wellTargetPoint = getWellPosition(
373367
labwareEntities[labwareId],
374368
wellTargetName,
375369
wellLocationOffset,
376-
addressableAreaOffset,
370+
fullOffset as CoordinateTuple,
377371
pipetteHasTip
378372
)
379373

@@ -385,14 +379,6 @@ export const getIsSafePipetteMovement = (args: {
385379
channels,
386380
})
387381

388-
const isWithinPipetteExtents = getIsWithinPipetteExtents(
389-
wellTargetPoint,
390-
nozzleConfiguration,
391-
primaryNozzle
392-
)
393-
if (!isWithinPipetteExtents) {
394-
return false
395-
}
396382
const tiprackEntity = tiprackId != null ? labwareEntities[tiprackId] : null
397383
const tipOverlapOnNozzle =
398384
tiprackEntity != null
@@ -409,6 +395,14 @@ export const getIsSafePipetteMovement = (args: {
409395
primaryNozzle,
410396
tipOverlapOnNozzle
411397
)
398+
const isWithinPipetteExtents = getIsMovementWithinDeckExtents({
399+
channels,
400+
boundingBox: pipetteBoundsAtWellLocation,
401+
robotType,
402+
})
403+
if (!isWithinPipetteExtents) {
404+
return false
405+
}
412406
const surroundingSlots =
413407
robotType === OT2_ROBOT_TYPE
414408
? getOt2SurroundingSlots(labwareSlot as OT2AddressableAreaName)
@@ -652,3 +646,44 @@ const getOverlapKeyForPipetteSpecs = (
652646
// default
653647
return 'Full'
654648
}
649+
650+
const getIsMovementWithinDeckExtents = (args: {
651+
channels: PipetteChannels
652+
boundingBox: Point[]
653+
robotType: RobotType
654+
}): boolean => {
655+
const { channels, boundingBox, robotType } = args
656+
const robotDef = getRobotDefFromRobotType(robotType)
657+
const { paddingOffsets } = robotDef
658+
const { front, rear, leftSide, rightSide } = paddingOffsets
659+
const [xExtent, yExtent] = robotDef.extents
660+
const [backLeftBound, frontRightBound] = boundingBox
661+
const { x: pipetteLeftBound, y: pipetteBackBound } = backLeftBound
662+
const { x: pipetteRightBound, y: pipetteFrontBound } = frontRightBound
663+
664+
if (channels === 96) {
665+
// check left
666+
if (pipetteRightBound < leftSide) {
667+
return false
668+
}
669+
// check right
670+
const rightLimit = xExtent + rightSide
671+
if (pipetteLeftBound > rightLimit) {
672+
return false
673+
}
674+
}
675+
676+
// 8- and 96-channel pipettes
677+
if (channels !== 1) {
678+
// check front
679+
if (pipetteBackBound < front) {
680+
return false
681+
}
682+
// check rear
683+
const rearLimit = yExtent + rear
684+
if (pipetteFrontBound > rearLimit) {
685+
return false
686+
}
687+
}
688+
return true
689+
}

0 commit comments

Comments
 (0)