Skip to content

Commit fe9b53b

Browse files
authored
feat(step-generation): ensure retract position is safe for air gap (#18730)
This PR introduces logic to check whether or not the retract position is safe for post-aspirate or -dispense air gaps. If the retract position is 2mm or more above the top of the well (deemed to be a "safe" position for air gap), we can move to the retract position for air gap. Otherwise, we move to the safe position above the well, and air gap there.
1 parent bd12ff6 commit fe9b53b

File tree

6 files changed

+291
-49
lines changed

6 files changed

+291
-49
lines changed

step-generation/src/commandCreators/compound/consolidate.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
curryWithoutPython,
2424
DEST_WELL_BLOWOUT_DESTINATION,
2525
formatPyStr,
26+
getIsRetractSafeForAirGap,
2627
getIsSafePipetteMovement,
2728
getSlotInLocationStack,
2829
getTransferPlanAndReferenceVolumes,
@@ -489,19 +490,40 @@ export const consolidate: CommandCreator<ConsolidateArgs> = (
489490
defaultValue: null,
490491
}) ?? dispenseFlowRateUlSec
491492

493+
const isDispenseRetractSafeForAirGap = getIsRetractSafeForAirGap({
494+
retractZOffset: dispenseRetractZOffset,
495+
retractPositionReference: dispenseRetractPositionReference,
496+
labwareId: destLabware,
497+
labwareEntities,
498+
well: destWell,
499+
})
500+
const preDispenseAirGapMoveToCommand =
501+
!isDispenseRetractSafeForAirGap && destWell != null
502+
? [
503+
curryWithoutPython(moveToWell, {
504+
pipetteId: pipette,
505+
labwareId: destLabware,
506+
wellName: destWell,
507+
wellLocation: SAFE_MOVE_TO_WELL_LOCATION,
508+
}),
509+
]
510+
: []
511+
492512
const jsonCommandCreators = flatMap(
493513
sourceWellChunks,
494514
(
495515
sourceWellChunk: string[],
496516
chunkIndex: number
497517
): CurriedCommandCreator[] => {
498518
const getAirGapAfterDispenseCommands = (
499-
considerUltimateSubtransfer: boolean
519+
considerUltimateSubtransfer: boolean,
520+
considerRetractSafety: boolean = true
500521
): CurriedCommandCreator[] =>
501522
dispenseAirGapVolume > 0 &&
502523
// don't air gap if end of full transfer and not changing tip
503524
!(changeTip === 'never' && isLastChunk && considerUltimateSubtransfer)
504525
? [
526+
...(considerRetractSafety ? preDispenseAirGapMoveToCommand : []),
505527
curryWithoutPython(prepareToAspirate, {
506528
pipetteId: pipette,
507529
}),
@@ -536,8 +558,9 @@ export const consolidate: CommandCreator<ConsolidateArgs> = (
536558
: {}),
537559
}),
538560
// move back to retract position after touch tip if air gap needed
561+
// if retract isn't safe for air gap, air gap commands will include a move to well safe position
539562
...(getAirGapAfterDispenseCommands(considerUltimateSubtransfer)
540-
.length > 0
563+
.length > 0 && isDispenseRetractSafeForAirGap
541564
? [
542565
curryWithoutPython(moveToWell, {
543566
pipetteId: pipette,
@@ -606,6 +629,24 @@ export const consolidate: CommandCreator<ConsolidateArgs> = (
606629
z: aspirateRetractZOffset,
607630
},
608631
}
632+
const isAspirateRetractSafeForAirGap = getIsRetractSafeForAirGap({
633+
retractZOffset: aspirateRetractZOffset,
634+
retractPositionReference: aspirateRetractPositionReference,
635+
labwareId: sourceLabware,
636+
labwareEntities,
637+
well: sourceWell,
638+
})
639+
const preAspirateAirGapMoveToCommand =
640+
!isAspirateRetractSafeForAirGap && sourceWell != null
641+
? [
642+
curryWithoutPython(moveToWell, {
643+
pipetteId: pipette,
644+
labwareId: sourceLabware,
645+
wellName: sourceWell,
646+
wellLocation: SAFE_MOVE_TO_WELL_LOCATION,
647+
}),
648+
]
649+
: []
609650
const dispenseCorrectionVolumeForDispenseAirGap =
610651
getByVolumeValue({
611652
liquidClass,
@@ -726,7 +767,8 @@ export const consolidate: CommandCreator<ConsolidateArgs> = (
726767
: {}),
727768
}),
728769
// move back to retract position after touch tip if air gap needed
729-
...(aspirateAirGapVolume > 0
770+
// if retract isn't safe for air gap, air gap commands will include a move to well safe position
771+
...(aspirateAirGapVolume > 0 && isAspirateRetractSafeForAirGap
730772
? [
731773
curryWithoutPython(moveToWell, {
732774
pipetteId: pipette,
@@ -751,6 +793,7 @@ export const consolidate: CommandCreator<ConsolidateArgs> = (
751793
const airGapAfterAspirateRetractCommands =
752794
aspirateAirGapVolume > 0
753795
? [
796+
...preAspirateAirGapMoveToCommand,
754797
curryWithoutPython(airGapInPlace, {
755798
pipetteId: pipette,
756799
volume: aspirateAirGapVolume,
@@ -1014,7 +1057,7 @@ export const consolidate: CommandCreator<ConsolidateArgs> = (
10141057
},
10151058
}),
10161059
...blowOutInPlaceCommand,
1017-
...getAirGapAfterDispenseCommands(true),
1060+
...getAirGapAfterDispenseCommands(true, false),
10181061
]
10191062
}
10201063

step-generation/src/commandCreators/compound/distribute.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
curryWithoutPython,
2525
DEST_WELL_BLOWOUT_DESTINATION,
2626
formatPyStr,
27+
getIsRetractSafeForAirGap,
2728
getIsSafePipetteMovement,
2829
getSlotInLocationStack,
2930
getTransferPlanAndReferenceVolumes,
@@ -507,6 +508,26 @@ export const distribute: CommandCreator<DistributeArgs> = (
507508
}) ?? dispenseFlowRateUlSec
508509

509510
const destWellChunks = chunk(destWells, numWellsToFitInTip)
511+
512+
const isAspirateRetractSafeForAirGap = getIsRetractSafeForAirGap({
513+
retractZOffset: aspirateRetractZOffset,
514+
retractPositionReference: aspirateRetractPositionReference,
515+
labwareId: sourceLabware,
516+
labwareEntities,
517+
well: sourceWell,
518+
})
519+
const preAspirateAirGapMoveToCommand =
520+
!isAspirateRetractSafeForAirGap && sourceWell != null
521+
? [
522+
curryWithoutPython(moveToWell, {
523+
pipetteId: pipette,
524+
labwareId: sourceLabware,
525+
wellName: sourceWell,
526+
wellLocation: SAFE_MOVE_TO_WELL_LOCATION,
527+
}),
528+
]
529+
: []
530+
510531
const jsonCommandCreators = flatMap(
511532
destWellChunks,
512533
(destWellChunk: string[], chunkIndex: number): CurriedCommandCreator[] => {
@@ -696,7 +717,8 @@ export const distribute: CommandCreator<DistributeArgs> = (
696717
: {}),
697718
}),
698719
// move back to retract position after touch tip if air gap needed
699-
...(aspirateAirGapVolume > 0
720+
// if retract isn't safe for air gap, air gap commands will include a move to well safe position
721+
...(aspirateAirGapVolume > 0 && isAspirateRetractSafeForAirGap
700722
? [
701723
curryWithoutPython(moveToWell, {
702724
pipetteId: pipette,
@@ -721,6 +743,7 @@ export const distribute: CommandCreator<DistributeArgs> = (
721743
const airGapAfterAspirateRetractCommands =
722744
aspirateAirGapVolume > 0
723745
? [
746+
...preAspirateAirGapMoveToCommand,
724747
curryWithoutPython(airGapInPlace, {
725748
pipetteId: pipette,
726749
volume: aspirateAirGapVolume,
@@ -833,6 +856,25 @@ export const distribute: CommandCreator<DistributeArgs> = (
833856
byVolumeProperty: 'correctionByVolume',
834857
defaultValue: 0,
835858
}) ?? 0
859+
860+
const isDispenseRetractSafeForAirGap = getIsRetractSafeForAirGap({
861+
retractZOffset: dispenseRetractZOffset,
862+
retractPositionReference: dispensePositionReference,
863+
labwareId: destLabware,
864+
labwareEntities,
865+
well: destinationWell,
866+
})
867+
const preDispenseAirGapMoveToCommand = !isDispenseRetractSafeForAirGap
868+
? [
869+
curryWithoutPython(moveToWell, {
870+
pipetteId: pipette,
871+
labwareId: destLabware,
872+
wellName: destinationWell,
873+
wellLocation: SAFE_MOVE_TO_WELL_LOCATION,
874+
}),
875+
]
876+
: []
877+
836878
const dispenseSubmergeCommands =
837879
destinationWell != null
838880
? [
@@ -951,7 +993,8 @@ export const distribute: CommandCreator<DistributeArgs> = (
951993
defaultValue: 0,
952994
}) ?? 0
953995
const getAirGapAfterDispenseCommands = (
954-
considerUltimateSubtransfer: boolean
996+
considerUltimateSubtransfer: boolean,
997+
considerRetractSafety: boolean = true
955998
): CurriedCommandCreator[] =>
956999
dispenseAirGapVolume > 0 &&
9571000
// don't air gap if not last well in chunk and conditioning volume is present
@@ -967,6 +1010,9 @@ export const distribute: CommandCreator<DistributeArgs> = (
9671010
considerUltimateSubtransfer
9681011
)
9691012
? [
1013+
...(considerRetractSafety
1014+
? preDispenseAirGapMoveToCommand
1015+
: []),
9701016
curryWithoutPython(prepareToAspirate, {
9711017
pipetteId: pipette,
9721018
}),
@@ -1001,9 +1047,10 @@ export const distribute: CommandCreator<DistributeArgs> = (
10011047
: {}),
10021048
}),
10031049
// move back to retract position after touch tip if air gap needed
1050+
// if retract isn't safe for air gap, air gap commands will include a move to well safe position
10041051
...(getAirGapAfterDispenseCommands(
10051052
considerUltimateSubtransfer
1006-
).length > 0
1053+
).length > 0 && isDispenseRetractSafeForAirGap
10071054
? [
10081055
curryWithoutPython(moveToWell, {
10091056
pipetteId: pipette,
@@ -1082,7 +1129,7 @@ export const distribute: CommandCreator<DistributeArgs> = (
10821129
}),
10831130
]
10841131
: []),
1085-
...finalAirGapAfterDispenseCommands,
1132+
...getAirGapAfterDispenseCommands(true, false),
10861133
]
10871134
} else {
10881135
// trash or waste chute
@@ -1102,7 +1149,7 @@ export const distribute: CommandCreator<DistributeArgs> = (
11021149
},
11031150
}),
11041151
...blowoutInPlaceCommand,
1105-
...getAirGapAfterDispenseCommands(true),
1152+
...getAirGapAfterDispenseCommands(true, false),
11061153
]
11071154
}
11081155

step-generation/src/commandCreators/compound/transfer.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
curryWithoutPython,
2121
DEST_WELL_BLOWOUT_DESTINATION,
2222
formatPyStr,
23+
getIsRetractSafeForAirGap,
2324
getSlotInLocationStack,
2425
getTrashOrLabware,
2526
indentPyLines,
@@ -489,22 +490,22 @@ export const transfer: CommandCreator<TransferArgs> = (
489490
]
490491
: []
491492

492-
const wellDepth =
493+
const aspirateWellDepth =
493494
labwareEntities[sourceLabware]?.def.wells[sourceWell]?.depth ?? null
494495
const aspirateMmFromBottom = getMmFromBottom(
495496
aspirateZOffset,
496497
aspiratePositionReference,
497-
wellDepth
498+
aspirateWellDepth
498499
)
499500
const aspirateSubmergeMmFromBottom = getMmFromBottom(
500501
aspirateSubmergeZOffset,
501502
aspirateSubmergePositionReference,
502-
wellDepth
503+
aspirateWellDepth
503504
)
504505
const aspirateRetractMmFromBottom = getMmFromBottom(
505506
aspirateRetractZOffset,
506507
aspirateRetractPositionReference,
507-
wellDepth
508+
aspirateWellDepth
508509
)
509510
if (
510511
aspirateMmFromBottom != null &&
@@ -513,6 +514,44 @@ export const transfer: CommandCreator<TransferArgs> = (
513514
) {
514515
errors.push(errorCreators.submergeBelowAspirate())
515516
}
517+
518+
const isAspirateRetractSafeForAirGap = getIsRetractSafeForAirGap({
519+
retractZOffset: aspirateRetractZOffset,
520+
retractPositionReference: aspirateRetractPositionReference,
521+
labwareId: sourceLabware,
522+
labwareEntities,
523+
well: sourceWell,
524+
})
525+
const preAspirateAirGapMoveToCommand =
526+
!isAspirateRetractSafeForAirGap && sourceWell != null
527+
? [
528+
curryWithoutPython(moveToWell, {
529+
pipetteId: pipette,
530+
labwareId: sourceLabware,
531+
wellName: sourceWell,
532+
wellLocation: SAFE_MOVE_TO_WELL_LOCATION,
533+
}),
534+
]
535+
: []
536+
537+
const isDispenseRetractSafeForAirGap = getIsRetractSafeForAirGap({
538+
retractZOffset: dispenseRetractZOffset,
539+
retractPositionReference: dispensePositionReference,
540+
labwareId: destLabware,
541+
labwareEntities,
542+
well: destinationWell,
543+
})
544+
const preDispenseAirGapMoveToCommand =
545+
!isDispenseRetractSafeForAirGap && destinationWell != null
546+
? [
547+
curryWithoutPython(moveToWell, {
548+
pipetteId: pipette,
549+
labwareId: destLabware,
550+
wellName: destinationWell,
551+
wellLocation: SAFE_MOVE_TO_WELL_LOCATION,
552+
}),
553+
]
554+
: []
516555
if (
517556
aspirateMmFromBottom != null &&
518557
aspirateRetractMmFromBottom != null &&
@@ -674,7 +713,8 @@ export const transfer: CommandCreator<TransferArgs> = (
674713
: {}),
675714
}),
676715
// move back to retract position after touch tip if air gap needed
677-
...(aspirateAirGapVol > 0
716+
// if retract isn't safe for air gap, air gap commands will include a move to well safe position
717+
...(aspirateAirGapVol > 0 && isAspirateRetractSafeForAirGap
678718
? [
679719
curryWithoutPython(moveToWell, {
680720
pipetteId: pipette,
@@ -699,6 +739,7 @@ export const transfer: CommandCreator<TransferArgs> = (
699739
const airGapAfterAspirateRetractCommands =
700740
aspirateAirGapVol > 0
701741
? [
742+
...preAspirateAirGapMoveToCommand,
702743
curryWithoutPython(airGapInPlace, {
703744
pipetteId: pipette,
704745
volume: aspirateAirGapVol,
@@ -906,7 +947,8 @@ export const transfer: CommandCreator<TransferArgs> = (
906947
}) ?? 0
907948

908949
const getAirGapAfterDispenseCommands = (
909-
considerUltimateSubtransfer: boolean
950+
considerUltimateSubtransfer: boolean,
951+
considerRetractSafety: boolean = true
910952
): CurriedCommandCreator[] =>
911953
dispenseAirGapVol > 0 &&
912954
!(
@@ -915,6 +957,9 @@ export const transfer: CommandCreator<TransferArgs> = (
915957
considerUltimateSubtransfer
916958
) // don't air gap if end of full transfer and not changing tip
917959
? [
960+
...(considerRetractSafety
961+
? preDispenseAirGapMoveToCommand
962+
: []),
918963
curryWithoutPython(airGapInPlace, {
919964
pipetteId: pipette,
920965
volume: dispenseAirGapVol,
@@ -947,9 +992,10 @@ export const transfer: CommandCreator<TransferArgs> = (
947992
: {}),
948993
}),
949994
// move back to retract position after touch tip if air gap needed
995+
// if retract isn't safe for air gap, air gap commands will include a move to well safe position
950996
...(getAirGapAfterDispenseCommands(
951997
considerUltimateSubtransfer
952-
).length > 0
998+
).length > 0 && isDispenseRetractSafeForAirGap
953999
? [
9541000
curryWithoutPython(moveToWell, {
9551001
pipetteId: pipette,
@@ -1017,7 +1063,7 @@ export const transfer: CommandCreator<TransferArgs> = (
10171063
}),
10181064
]
10191065
: []),
1020-
...getAirGapAfterDispenseCommands(true),
1066+
...getAirGapAfterDispenseCommands(true, false),
10211067
]
10221068
break
10231069
default:
@@ -1031,7 +1077,7 @@ export const transfer: CommandCreator<TransferArgs> = (
10311077
flowRate: blowoutFlowRateUlSec,
10321078
trashId: blowoutLocation,
10331079
}),
1034-
...getAirGapAfterDispenseCommands(true),
1080+
...getAirGapAfterDispenseCommands(true, false),
10351081
]
10361082
} else if (blowoutLocation in wasteChuteEntities) {
10371083
advancedDispenseArgsCommands = [
@@ -1040,7 +1086,7 @@ export const transfer: CommandCreator<TransferArgs> = (
10401086
flowRate: blowoutFlowRateUlSec,
10411087
wasteChuteId: blowoutLocation,
10421088
}),
1043-
...getAirGapAfterDispenseCommands(true),
1089+
...getAirGapAfterDispenseCommands(true, false),
10441090
]
10451091
}
10461092
break

0 commit comments

Comments
 (0)