Skip to content

Commit

Permalink
feat(protocol-designer): add air gap form validation (#6226)
Browse files Browse the repository at this point in the history
6007
  • Loading branch information
shlokamin authored Aug 3, 2020
1 parent 762a956 commit 46958b3
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 16 deletions.
12 changes: 12 additions & 0 deletions protocol-designer/src/pipettes/pipetteData.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,15 @@ export function getPipetteCapacity(pipetteEntity: PipetteEntity): number {
)
return NaN
}

export function getMinPipetteVolume(pipetteEntity: PipetteEntity): number {
const spec = pipetteEntity.spec
if (spec) {
return spec.minVolume
}
assert(
false,
`Expected spec for pipette ${pipetteEntity ? pipetteEntity.id : '???'}`
)
return NaN
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {
THERMOCYCLER_MODULE_V1,
} from '@opentrons/shared-data'
import { fixtureP10Single } from '@opentrons/shared-data/pipette/fixtures/name'
import fixture_tiprack_10_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul'
import { getPrereleaseFeatureFlag } from '../../persist'
import { getStateAndContextTempTCModules } from '../../step-generation/__fixtures__'
import {
createPresavedStepForm,
type CreatePresavedStepFormArgs,
} from '../utils/createPresavedStepForm'

jest.mock('../../persist')

const mockGetPrereleaseFeatureFlag: JestMockFn<
Expand All @@ -34,6 +34,7 @@ beforeEach(() => {
name: 'p10_single',
id: 'leftPipetteId',
spec: fixtureP10Single,
tiprackLabwareDef: fixture_tiprack_10_ul,
}
const labwareOnMagModule = {
id: 'labwareOnMagModule',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import type {
LabwareEntities,
PipetteEntities,
} from '../../../step-forms/types'
import {
getMinPipetteVolume,
getPipetteCapacity,
} from '../../../pipettes/pipetteData'

// TODO: Ian 2019-02-21 import this from a more central place - see #2926
const getDefaultFields = (...fields: Array<StepFieldName>): FormPatch =>
Expand Down Expand Up @@ -222,6 +226,36 @@ const updatePatchOnPipetteChange = (
const getClearedDisposalVolumeFields = () =>
getDefaultFields('disposalVolume_volume', 'disposalVolume_checkbox')

const clampAirGapVolume = (
patch: FormPatch,
rawForm: FormData,
pipetteEntities: PipetteEntities
): FormPatch => {
const patchedAspirateAirgapVolume = patch.aspirate_airGap_volume
const pipetteId = patch.pipette || rawForm.pipette

if (
patchedAspirateAirgapVolume &&
typeof pipetteId === 'string' &&
pipetteId in pipetteEntities
) {
const pipetteEntity = pipetteEntities[pipetteId]
const minPipetteVolume = getMinPipetteVolume(pipetteEntity)
const minAirGapVolume = 0 // NOTE: a form level warning will occur if the air gap volume is below the pipette min volume
const maxAirGapVolume = getPipetteCapacity(pipetteEntity) - minPipetteVolume
const clampedAirGapVolume = clamp(
Number(patchedAspirateAirgapVolume),
minAirGapVolume,
maxAirGapVolume
)
return {
...patch,
aspirate_airGap_volume: String(clampedAirGapVolume),
}
}
return patch
}

const updatePatchDisposalVolumeFields = (
patch: FormPatch,
rawForm: FormData,
Expand Down Expand Up @@ -511,6 +545,7 @@ export function dependentFieldsUpdateMoveLiquid(
chainPatch =>
updatePatchDisposalVolumeFields(chainPatch, rawForm, pipetteEntities),
chainPatch => clampDisposalVolume(chainPatch, rawForm, pipetteEntities),
chainPatch => clampAirGapVolume(chainPatch, rawForm, pipetteEntities),
chainPatch => updatePatchMixFields(chainPatch, rawForm),
chainPatch => updatePatchBlowoutFields(chainPatch, rawForm),
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,19 +268,84 @@ describe('disposal volume should update...', () => {
})

describe('air gap volume', () => {
const form = {
path: 'multiDispense',
aspirate_wells: ['A1'],
dispense_wells: ['B2', 'B3'],
volume: '2',
pipette: 'pipetteId',
disposalVolume_checkbox: true,
disposalVolume_volume: '1.1',
aspirate_airGap_checkbox: false,
aspirate_airGap_volume: null,
}
it('should reset to pipette min when pipette is changed', () => {
const result = handleFormHelper({ pipette: 'otherPipetteId' }, form)
expect(result).toMatchObject({ aspirate_airGap_volume: '30' })
describe('when the path is single', () => {
let form
beforeEach(() => {
form = {
path: 'single',
aspirate_wells: ['A1'],
dispense_wells: ['B2'],
volume: '2',
pipette: 'pipetteId',
disposalVolume_checkbox: true,
disposalVolume_volume: '1.1',
}
})

it('should update the air gap volume to 0 when the patch volume is less than 0', () => {
const result = handleFormHelper({ aspirate_airGap_volume: '-1' }, form)
expect(result.aspirate_airGap_volume).toEqual('0')
})
it('should update the air gap volume to the pipette capacity - min pipette volume when the air gap volume is too big', () => {
const result = handleFormHelper({ aspirate_airGap_volume: '100' }, form)
expect(result.aspirate_airGap_volume).toEqual('9')
})
it('should NOT update when the patch volume is greater than the min pipette volume', () => {
const result = handleFormHelper({ aspirate_airGap_volume: '2' }, form)
expect(result.aspirate_airGap_volume).toEqual('2')
})
it('should NOT update when the patch volume is equal to the min pipette volume', () => {
const result = handleFormHelper({ aspirate_airGap_volume: '1' }, form)
expect(result.aspirate_airGap_volume).toEqual('1')
})
})

describe('when the path is multi aspirate', () => {
let form
beforeEach(() => {
form = {
path: 'multiAspirate',
aspirate_wells: ['A1', 'B1'],
dispense_wells: ['B2'],
volume: '2',
pipette: 'pipetteId',
disposalVolume_checkbox: true,
disposalVolume_volume: '1.1',
}
})

it('should update the air gap volume to 0 when the patch volume is less than 0', () => {
const result = handleFormHelper({ aspirate_airGap_volume: '-1' }, form)
expect(result.aspirate_airGap_volume).toEqual('0')
})
it('should update the air gap volume to the pipette capacity - min pipette volume when the air gap volume is too big', () => {
const result = handleFormHelper({ aspirate_airGap_volume: '100' }, form)
expect(result.aspirate_airGap_volume).toEqual('9')
})
it('should NOT update when the patch volume is greater than the min pipette volume', () => {
const result = handleFormHelper({ aspirate_airGap_volume: '2' }, form)
expect(result.aspirate_airGap_volume).toEqual('2')
})
it('should NOT update when the patch volume is equal to the min pipette volume', () => {
const result = handleFormHelper({ aspirate_airGap_volume: '1' }, form)
expect(result.aspirate_airGap_volume).toEqual('1')
})
})
describe('when the path is multi dispense', () => {
const form = {
path: 'multiDispense',
aspirate_wells: ['A1'],
dispense_wells: ['B2', 'B3'],
volume: '2',
pipette: 'pipetteId',
disposalVolume_checkbox: true,
disposalVolume_volume: '1.1',
aspirate_airGap_checkbox: false,
aspirate_airGap_volume: null,
}
it('should reset to pipette min when pipette is changed', () => {
const result = handleFormHelper({ pipette: 'otherPipetteId' }, form)
expect(result).toMatchObject({ aspirate_airGap_volume: '30' })
})
})
})
4 changes: 3 additions & 1 deletion protocol-designer/src/steplist/formLevel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
minDisposalVolume,
type FormWarning,
type FormWarningType,
minAirGapVolume,
} from './warnings'
import type { StepType } from '../../form-types'

Expand Down Expand Up @@ -65,7 +66,8 @@ const stepFormHelperMap: { [StepType]: FormHelpers } = {
getWarnings: composeWarnings(
belowPipetteMinimumVolume,
maxDispenseWellVolume,
minDisposalVolume
minDisposalVolume,
minAirGapVolume
),
},
magnet: {
Expand Down
56 changes: 56 additions & 0 deletions protocol-designer/src/steplist/formLevel/test/warnings.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// @flow
import { minAirGapVolume } from '../warnings'

describe('warnings', () => {
let pipette
beforeEach(() => {
pipette = {
spec: {
minVolume: 100,
},
}
})
describe('min air gap volume', () => {
it('should NOT return a warning when the air gap checkbox is not selected', () => {
const fields = {
aspirate_airGap_checkbox: false,
aspirate_airGap_volume: null,
...{ pipette },
}
expect(minAirGapVolume({ ...fields })).toBe(null)
})
it('should NOT return a warning when there is no air gap volume specified', () => {
const fields = {
aspirate_airGap_checkbox: true,
aspirate_airGap_volume: null,
...{ pipette },
}
expect(minAirGapVolume({ ...fields })).toBe(null)
})
it('should NOT return a warning when the air gap volume is greater than the pipette min volume', () => {
const fields = {
aspirate_airGap_checkbox: true,
aspirate_airGap_volume: '150',
...{ pipette },
}
expect(minAirGapVolume(fields)).toBe(null)
})

it('should NOT return a warning when the air gap volume is equal to the the pipette min volume', () => {
const fields = {
aspirate_airGap_checkbox: true,
aspirate_airGap_volume: '100',
...{ pipette },
}
expect(minAirGapVolume(fields)).toBe(null)
})
it('should return a warning when the air gap volume is less than the pipette min volume', () => {
const fields = {
aspirate_airGap_checkbox: true,
aspirate_airGap_volume: '0',
...{ pipette },
}
expect(minAirGapVolume(fields).type).toBe('BELOW_MIN_AIR_GAP_VOLUME')
})
})
})
26 changes: 26 additions & 0 deletions protocol-designer/src/steplist/formLevel/warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,25 @@ export type FormWarningType =
| 'BELOW_PIPETTE_MINIMUM_VOLUME'
| 'OVER_MAX_WELL_VOLUME'
| 'BELOW_MIN_DISPOSAL_VOLUME'
| 'BELOW_MIN_AIR_GAP_VOLUME'

export type FormWarning = {
...$Exact<FormError>,
type: FormWarningType,
}
// TODO: Ian 2018-12-06 use i18n for title/body text
const FORM_WARNINGS: { [FormWarningType]: FormWarning } = {
BELOW_MIN_AIR_GAP_VOLUME: {
type: 'BELOW_MIN_AIR_GAP_VOLUME',
title: 'Below recommended air gap',
body: (
<React.Fragment>
For accuracy while using air gap we recommend you use a volume of at
least the pipette&apos;s minimum.
</React.Fragment>
),
dependentFields: ['disposalVolume_volume', 'pipette'],
},
BELOW_PIPETTE_MINIMUM_VOLUME: {
type: 'BELOW_PIPETTE_MINIMUM_VOLUME',
title: 'Specified volume is below pipette minimum',
Expand Down Expand Up @@ -87,6 +99,20 @@ export const minDisposalVolume = (fields: HydratedFormData): ?FormWarning => {
return isBelowMin ? FORM_WARNINGS.BELOW_MIN_DISPOSAL_VOLUME : null
}

export const minAirGapVolume = (fields: HydratedFormData): ?FormWarning => {
const { aspirate_airGap_checkbox, aspirate_airGap_volume, pipette } = fields
if (
!aspirate_airGap_checkbox ||
!aspirate_airGap_volume ||
!pipette ||
!pipette.spec
)
return null

const isBelowMin = Number(aspirate_airGap_volume) < pipette.spec.minVolume
return isBelowMin ? FORM_WARNINGS.BELOW_MIN_AIR_GAP_VOLUME : null
}

/*******************
** Helpers **
********************/
Expand Down

0 comments on commit 46958b3

Please sign in to comment.