Skip to content

Commit 235cde4

Browse files
committed
feat(protocol-designer,-shared-data): introduce push out field in PD
This PR introduces UI and form data for push out as a checkbox expand form field in moveLiquid -> dispense. Previously, push out volumes were not configurable by the user, and were added implicitly. In order to migrate older protocols, we need to grab their default pushouts from their pipette specs. The utility `getDefaultPushOutVolume` takes into account transfer volume in order to access the correct liquids properties (default or low volume). Closes AUTH-911
1 parent 13000b5 commit 235cde4

File tree

20 files changed

+279
-31
lines changed

20 files changed

+279
-31
lines changed

protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,9 @@
226226
"stepName": "transfer",
227227
"stepDetails": "",
228228
"id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5",
229-
"dispense_touchTip_mmfromTop": -2
229+
"dispense_touchTip_mmfromTop": -2,
230+
"pushOut_checkbox": false,
231+
"pushOut_volume": 0
230232
},
231233
"54dc3200-75c7-11ea-b42f-4b64e50f43e5": {
232234
"moduleId": null,

protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,9 @@
258258
"stepName": "transfer",
259259
"stepDetails": "",
260260
"id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5",
261-
"dispense_touchTip_mmfromTop": null
261+
"dispense_touchTip_mmfromTop": null,
262+
"pushOut_checkbox": false,
263+
"pushOut_volume": 0
262264
},
263265
"4f4057e0-75c7-11ea-b42f-4b64e50f43e5": {
264266
"engageHeight": "6",

protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,9 @@
276276
"stepName": "transfer",
277277
"stepDetails": "",
278278
"id": "f9a294f1-f42b-4cae-893a-592405349d56",
279-
"dispense_touchTip_mmfromTop": null
279+
"dispense_touchTip_mmfromTop": null,
280+
"pushOut_checkbox": true,
281+
"pushOut_volume": 7
280282
},
281283
"5fdb9a12-fab4-42fd-886f-40af107b15d6": {
282284
"aspirate_delay_checkbox": false,

protocol-designer/fixtures/protocol/8/doItAllV8.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,9 @@
239239
"stepName": "transfer",
240240
"stepDetails": "",
241241
"id": "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7",
242-
"dispense_touchTip_mmfromTop": null
242+
"dispense_touchTip_mmfromTop": null,
243+
"pushOut_checkbox": true,
244+
"pushOut_volume": 7
243245
},
244246
"240a2c96-3db8-4679-bdac-049306b7b9c4": {
245247
"blockIsActive": true,

protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@
206206
"stepName": "transfer things",
207207
"stepDetails": "yeah notes",
208208
"id": "e7d36200-92a5-11e9-ac62-1b173f839d9e",
209-
"dispense_touchTip_mmfromTop": null
209+
"dispense_touchTip_mmfromTop": null,
210+
"pushOut_checkbox": false,
211+
"pushOut_volume": 0
210212
},
211213
"18113c80-92a6-11e9-ac62-1b173f839d9e": {
212214
"aspirate_delay_checkbox": false,

protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@
140140
"stepName": "transfer",
141141
"stepDetails": "",
142142
"dispense_touchTip_mmfromTop": null,
143-
"id": "292e8b18-f59e-4c63-b0f3-e242bf50094b"
143+
"id": "292e8b18-f59e-4c63-b0f3-e242bf50094b",
144+
"pushOut_checkbox": true,
145+
"pushOut_volume": 2
144146
},
145147
"960c2d3b-9cf9-49b0-ab4c-af4113f6671a": {
146148
"moduleId": "d6966555-6c0e-45e0-8056-428d7c486401:temperatureModuleType",

protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@
139139
"stepName": "transfer",
140140
"stepDetails": "",
141141
"dispense_touchTip_mmfromTop": null,
142-
"id": "83a095fa-b649-4105-99d4-177f1a3f363a"
142+
"id": "83a095fa-b649-4105-99d4-177f1a3f363a",
143+
"pushOut_checkbox": true,
144+
"pushOut_volume": 7
143145
},
144146
"f5ea3139-1585-4848-9d5f-832eb88c99ca": {
145147
"aspirate_airGap_checkbox": false,
@@ -229,7 +231,9 @@
229231
"stepName": "transfer",
230232
"stepDetails": "",
231233
"dispense_touchTip_mmfromTop": null,
232-
"id": "f5ea3139-1585-4848-9d5f-832eb88c99ca"
234+
"id": "f5ea3139-1585-4848-9d5f-832eb88c99ca",
235+
"pushOut_checkbox": true,
236+
"pushOut_volume": 7
233237
}
234238
},
235239
"orderedStepIds": [

protocol-designer/src/assets/localization/en/form.json

+7
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,13 @@
235235
"preWetTip": {
236236
"label": "pre-wet tip"
237237
},
238+
"pushOut": {
239+
"pushOut_volume": {
240+
"caption": "Valid range between {{min}}-{{max}}µL",
241+
"label": "Push out volume"
242+
},
243+
"title": "Push out"
244+
},
238245
"setTemperature": {
239246
"options": {
240247
"false": "Deactivate module",

protocol-designer/src/assets/localization/en/tooltip.json

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"nozzles": "Partial pickup requires a tip rack directly on the deck. Full rack pickup requires the Flex 96 Tip Rack Adapter.",
7373
"pipette": "Select the pipette you want to use",
7474
"preWetTip": "Pre-wet by aspirating and dispensing the total aspiration volume",
75+
"pushOut_checkbox": "Helps ensure all liquid leaves the tip",
7576
"setTemperature": "Select the temperature to set your module to",
7677
"wells": "Select wells",
7778
"volume": "Volume to dispense in each well"

protocol-designer/src/load-file/migration/8_5_0.ts

+24-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import floor from 'lodash/floor'
2+
import { getPipetteSpecsV2 } from '@opentrons/shared-data'
23
import { PROTOCOL_DESIGNER_SOURCE } from '../../constants'
34
import { swatchColors } from '../../components/organisms/DefineLiquidsModal/swatchColors'
5+
import { getDefaultPushOutVolume } from '../../utils'
46
import { getMigratedPositionFromTop } from './utils/getMigrationPositionFromTop'
57
import { getAdditionalEquipmentLocationUpdate } from './utils/getAdditionalEquipmentLocationUpdate'
68
import { getEquipmentLoadInfoFromCommands } from './utils/getEquipmentLoadInfoFromCommands'
@@ -22,15 +24,10 @@ export const migrateFile = (
2224
liquids,
2325
robot,
2426
} = appData
25-
2627
if (designerApplication == null || designerApplication?.data == null) {
2728
throw Error('The designerApplication key in your file is corrupt.')
2829
}
29-
const savedStepForms = designerApplication.data
30-
?.savedStepForms as DesignerApplicationData['savedStepForms']
31-
32-
const ingredients = designerApplication.data.ingredients
33-
30+
const { savedStepForms, ingredients } = designerApplication.data
3431
const migratedIngredients: Ingredients = Object.entries(
3532
ingredients
3633
).reduce<Ingredients>((acc, [id, ingredient]) => {
@@ -48,6 +45,10 @@ export const migrateFile = (
4845
(command): command is LoadLabwareCreateCommand =>
4946
command.commandType === 'loadLabware'
5047
)
48+
const equipmentLoadInfoFromCommands = getEquipmentLoadInfoFromCommands(
49+
commands,
50+
labwareDefinitions
51+
)
5152

5253
const savedStepsWithUpdatedMoveLiquidFields = Object.values(
5354
savedStepForms
@@ -74,6 +75,20 @@ export const migrateFile = (
7475
dispense_labware as string,
7576
'dispense'
7677
)
78+
const tipRackDef = labwareDefinitions[form.tipRack]
79+
const pipetteName =
80+
equipmentLoadInfoFromCommands.pipettes?.[form.pipette]?.pipetteName ??
81+
null
82+
const pipetteSpecs =
83+
pipetteName != null ? getPipetteSpecsV2(pipetteName) : null
84+
const defaultPushOutVolume =
85+
pipetteSpecs == null
86+
? null
87+
: getDefaultPushOutVolume(
88+
Number(form.volume),
89+
pipetteSpecs,
90+
tipRackDef
91+
)
7792

7893
return {
7994
...acc,
@@ -124,6 +139,9 @@ export const migrateFile = (
124139
dispense_submerge_position_reference: null,
125140
liquidClassesSupported: liquidClassesSupported ?? false,
126141
liquidClass: null,
142+
pushOut_checkbox:
143+
defaultPushOutVolume != null && defaultPushOutVolume > 0,
144+
pushOut_volume: defaultPushOutVolume,
127145
},
128146
}
129147
}
@@ -188,10 +206,6 @@ export const migrateFile = (
188206
},
189207
{}
190208
)
191-
const equipmentLoadInfoFromCommands = getEquipmentLoadInfoFromCommands(
192-
commands,
193-
labwareDefinitions
194-
)
195209
return {
196210
...appData,
197211
metadata: {

protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/SecondStepsMoveLiquidTools.tsx

+40-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import {
2929
import {
3030
getAdditionalEquipmentEntities,
3131
getLabwareEntities,
32+
getPipetteEntities,
3233
} from '../../../../../../step-forms/selectors'
34+
import { getMaxPushOutVolume } from '../../../../../../utils'
3335
import {
3436
getBlowoutLocationOptionsForForm,
3537
getFormErrorsMappedToField,
@@ -70,7 +72,7 @@ export const SecondStepsMoveLiquidTools = ({
7072
getAdditionalEquipmentEntities
7173
)
7274
const enableLiquidClasses = useSelector(getEnableLiquidClasses)
73-
75+
const pipetteSpec = useSelector(getPipetteEntities)[formData.pipette]?.spec
7476
const addFieldNamePrefix = addPrefix(tab)
7577
const isWasteChuteSelected =
7678
propsForFields.dispense_labware?.value != null
@@ -147,6 +149,11 @@ export const SecondStepsMoveLiquidTools = ({
147149
]
148150
}
149151

152+
const maxPushoutVolume = getMaxPushOutVolume(
153+
Number(formData.volume),
154+
pipetteSpec
155+
)
156+
150157
const minXYDimension = getMinXYDimension(
151158
labwares[formData[`${tab}_labware`]].def,
152159
['A1']
@@ -318,6 +325,38 @@ export const SecondStepsMoveLiquidTools = ({
318325
</Flex>
319326
) : null}
320327
</CheckboxExpandStepFormField>
328+
{tab === 'dispense' ? (
329+
<CheckboxExpandStepFormField
330+
title={i18n.format(
331+
t('form:step_edit_form.field.pushOut.title'),
332+
'capitalize'
333+
)}
334+
checkboxValue={propsForFields.pushOut_checkbox.value}
335+
isChecked={propsForFields.pushOut_checkbox.value === true}
336+
checkboxUpdateValue={propsForFields.pushOut_checkbox.updateValue}
337+
tooltipText={propsForFields.pushOut_checkbox.tooltipContent}
338+
>
339+
{formData.pushOut_checkbox === true ? (
340+
<InputStepFormField
341+
showTooltip={false}
342+
padding="0"
343+
title={t(
344+
'form:step_edit_form.field.pushOut.pushOut_volume.label'
345+
)}
346+
caption={t(
347+
'form:step_edit_form.field.pushOut.pushOut_volume.caption',
348+
{ min: 0, max: maxPushoutVolume }
349+
)}
350+
{...propsForFields.pushOut_volume}
351+
units={t('application:units.microliter')}
352+
errorToShow={getFormLevelError(
353+
'pushOut_volume',
354+
mappedErrorsToField
355+
)}
356+
/>
357+
) : null}
358+
</CheckboxExpandStepFormField>
359+
) : null}
321360
<CheckboxExpandStepFormField
322361
title={i18n.format(
323362
t('form:step_edit_form.field.delay.label'),

protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ describe('createPresavedStepForm', () => {
217217
disposalVolume_volume: '1',
218218
path: 'single',
219219
preWetTip: false,
220+
pushOut_checkbox: null,
221+
pushOut_volume: null,
220222
stepDetails: '',
221223
stepName: 'transfer',
222224
volume: null,

protocol-designer/src/steplist/formLevel/errors.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
THERMOCYCLER_PROFILE,
2020
} from '../../constants'
2121
import { getPipetteCapacity } from '../../pipettes/pipetteData'
22-
import { canPipetteUseLabware } from '../../utils'
22+
import { canPipetteUseLabware, getMaxPushOutVolume } from '../../utils'
2323
import { getWellRatio } from '../utils'
2424
import { getTimeFromForm } from '../utils/getTimeFromForm'
2525

@@ -423,41 +423,55 @@ const ASPIRATE_TOUCH_TIP_SPEED_REQUIRED: FormError = {
423423
showAtForm: false,
424424
showAtField: true,
425425
page: 2,
426+
tab: 'aspirate',
426427
}
427428
const DISPENSE_TOUCH_TIP_SPEED_REQUIRED: FormError = {
428429
title: 'Touch tip speed required',
429430
dependentFields: ['dispense_touchTip_speed'],
430431
showAtForm: false,
431432
showAtField: true,
432433
page: 2,
434+
tab: 'dispense',
433435
}
434436
const ASPIRATE_TOUCH_TIP_MM_FROM_EDGE_OUT_OF_RANGE: FormError = {
435437
title: 'Value falls outside of accepted range',
436438
dependentFields: ['aspirate_touchTip_mmFromEdge'],
437439
showAtForm: false,
438440
showAtField: true,
439441
page: 2,
442+
tab: 'aspirate',
440443
}
441444
const DISPENSE_TOUCH_TIP_MM_FROM_EDGE_OUT_OF_RANGE: FormError = {
442445
title: 'Value falls outside of accepted range',
443446
dependentFields: ['dispense_touchTip_mmFromEdge'],
444447
showAtForm: false,
445448
showAtField: true,
446449
page: 2,
450+
tab: 'dispense',
447451
}
448452
const ASPIRATE_TOUCH_TIP_MM_FROM_EDGE_REQUIRED: FormError = {
449453
title: 'Value required',
450454
dependentFields: ['aspirate_touchTip_mmFromEdge'],
451455
showAtForm: false,
452456
showAtField: true,
453457
page: 2,
458+
tab: 'aspirate',
454459
}
455460
const DISPENSE_TOUCH_TIP_MM_FROM_EDGE_REQUIRED: FormError = {
456461
title: 'Value required',
457462
dependentFields: ['dispense_touchTip_mmFromEdge'],
458463
showAtForm: false,
459464
showAtField: true,
460465
page: 2,
466+
tab: 'dispense',
467+
}
468+
const PUSH_OUT_VOLUME_REQUIRED: FormError = {
469+
title: 'Push out volume required',
470+
dependentFields: ['pushOut_volume'],
471+
showAtForm: false,
472+
showAtField: true,
473+
page: 2,
474+
tab: 'dispense',
461475
}
462476

463477
export interface HydratedFormData {
@@ -1073,6 +1087,27 @@ export const dispenseTouchTipMmFromEdgeRequired = (
10731087
? DISPENSE_TOUCH_TIP_MM_FROM_EDGE_REQUIRED
10741088
: null
10751089
}
1090+
export const pushOutVolumeRequired = (
1091+
fields: HydratedFormData
1092+
): FormError | null => {
1093+
const { pushOut_checkbox, pushOut_volume } = fields
1094+
return pushOut_checkbox && !pushOut_volume ? PUSH_OUT_VOLUME_REQUIRED : null
1095+
}
1096+
export const pushOutVolumeOutOfRange = (
1097+
fields: HydratedFormData
1098+
): FormError | null => {
1099+
const { pushOut_checkbox, pushOut_volume, pipette, volume } = fields
1100+
if (pipette == null) {
1101+
return null
1102+
}
1103+
const maxPushOutVolume = getMaxPushOutVolume(
1104+
Number(volume),
1105+
(pipette as PipetteEntity).spec
1106+
)
1107+
return pushOut_checkbox && pushOut_volume > maxPushOutVolume
1108+
? PUSH_OUT_VOLUME_REQUIRED
1109+
: null
1110+
}
10761111

10771112
/*******************
10781113
** Helpers **

protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts

+2
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ export function getDefaultsForStepType(
135135
pickUpTip_wellNames: undefined,
136136
pipette: null,
137137
preWetTip: false,
138+
pushOut_checkbox: null,
139+
pushOut_volume: null,
138140
tipRack: null,
139141
volume: null,
140142
}

protocol-designer/src/steplist/formLevel/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import {
5555
dispenseTouchTipMmFromEdgeOutOfRange,
5656
aspirateTouchTipMmFromEdgeRequired,
5757
dispenseTouchTipMmFromEdgeRequired,
58+
pushOutVolumeRequired,
59+
pushOutVolumeOutOfRange,
5860
} from './errors'
5961

6062
import {
@@ -159,6 +161,8 @@ const stepFormHelperMap: Partial<Record<StepType, FormHelpers>> = {
159161
dispenseWellsRequired,
160162
aspirateTouchTipSpeedRequired,
161163
dispenseTouchTipSpeedRequired,
164+
pushOutVolumeRequired,
165+
pushOutVolumeOutOfRange,
162166
aspirateTouchTipMmFromEdgeOutOfRange,
163167
dispenseTouchTipMmFromEdgeOutOfRange,
164168
aspirateTouchTipMmFromEdgeRequired,

protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ describe('getDefaultsForStepType', () => {
8484
blowout_location: null,
8585
blowout_flowRate: null,
8686
preWetTip: false,
87+
pushOut_checkbox: null,
88+
pushOut_volume: null,
8789

8890
aspirate_airGap_checkbox: false,
8991
aspirate_airGap_volume: null,

0 commit comments

Comments
 (0)