Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol-designer): add air gap form validation #6226

Merged
merged 1 commit into from
Aug 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -224,6 +228,36 @@ const clearedDisposalVolumeFields = getDefaultFields(
'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 @@ -513,6 +547,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