Skip to content

Commit fa27d82

Browse files
authored
fix(app): fix air gap volume range calc (#19111)
* fix(app): fix air gap volume range calc
1 parent c615e99 commit fa27d82

File tree

6 files changed

+84
-42
lines changed

6 files changed

+84
-42
lines changed

app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics'
2222
import { ANALYTICS_QUICK_TRANSFER_SETTING_SAVED } from '/app/redux/analytics'
2323

2424
import { ACTIONS } from '../constants'
25+
import {
26+
getAspirateAirGapVolumeRange,
27+
getDispenseAirGapVolumeRange,
28+
} from '../utils'
2529

2630
import type { Dispatch } from 'react'
2731
import type {
@@ -110,40 +114,18 @@ export function AirGap(props: AirGapProps): JSX.Element {
110114
const setSaveOrContinueButtonText =
111115
airGapEnabled && currentStep < 2 ? t('shared:continue') : t('shared:save')
112116

113-
const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume
114-
const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume
115-
116-
// dispense air gap is performed whenever a tip is on its way to the trash, so
117-
// we can have the max be at the max tip capacity
118-
let maxAvailableCapacity = Math.min(maxPipetteVolume, tipVolume)
119-
console.log('maxAvailableCapacity', maxAvailableCapacity)
120-
console.log('path', state.path)
121-
console.log('volume', state.volume)
122-
123-
// for aspirate, air gap behaves differently depending on the path
124-
if (kind === 'aspirate') {
125-
if (state.path === 'single') {
126-
// for a single path, air gap capacity is just the difference between the
127-
// pipette/tip capacity and the volume per well
128-
maxAvailableCapacity =
129-
Math.min(maxPipetteVolume, tipVolume) - state.volume
130-
} else if (state.path === 'multiAspirate') {
131-
// an aspirate air gap for multi aspirate will aspirate an air gap
132-
// after each aspirate action, so we need to halve the available capacity for single path
133-
// to get the amount available, assuming a min of 2 aspirates per dispense
134-
maxAvailableCapacity =
135-
(Math.min(maxPipetteVolume, tipVolume) - 2 * state.volume) / 2
136-
} else {
137-
// aspirate air gap for multi dispense occurs once per asprirate and
138-
// available volume is max capacity - volume*3 assuming a min of 2 dispenses
139-
// per aspirate plus 1x the volume for disposal
140-
maxAvailableCapacity =
141-
Math.min(maxPipetteVolume, tipVolume) - state.volume * 3
142-
}
143-
}
144-
145-
const volumeRange = { min: 1, max: Math.floor(maxAvailableCapacity) }
146-
console.log(volumeRange)
117+
const { min, max } =
118+
kind === 'aspirate'
119+
? getAspirateAirGapVolumeRange(state.pipette, state.tipRack)
120+
: getDispenseAirGapVolumeRange(
121+
state.volume,
122+
state?.disposalVolume ?? 0,
123+
state.path,
124+
state.pipette,
125+
state.tipRack
126+
)
127+
128+
const volumeRange = { min, max }
147129
let volumeError = null
148130
if (volumeRange.min > volumeRange.max) {
149131
volumeError = t('air_gap_capacity_error')

app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/AirGap.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe('AirGap', () => {
7676
fireEvent.click(screen.getByRole('button', { name: '1' }))
7777
fireEvent.click(screen.getByRole('button', { name: '1' }))
7878
fireEvent.click(screen.getByText('Save'))
79-
screen.getByText('Value must be between 1 to 5')
79+
screen.getByText('Value must be between 0 to 9')
8080
})
8181

8282
it('should call dispatch when clicking save button aspirate', () => {

app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/AirGap.test.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ describe('AirGap', () => {
4949
] as any,
5050
} as any,
5151
tipRack: {
52+
parameters: {
53+
isTiprack: true,
54+
},
5255
wells: {
5356
A1: {
5457
totalLiquidVolume: 200,
@@ -119,15 +122,16 @@ describe('AirGap', () => {
119122
fireEvent.click(enabledBtn)
120123
const continueBtn = screen.getByText('Continue')
121124
fireEvent.click(continueBtn)
122-
const numButton = screen.getByText('0')
123-
fireEvent.click(numButton)
125+
fireEvent.click(screen.getByText('2'))
126+
fireEvent.click(screen.getByText('0'))
127+
fireEvent.click(screen.getByText('0'))
124128
expect(vi.mocked(InputField)).toHaveBeenCalledWith(
125129
{
126130
title: 'Air gap volume (µL)',
127-
error: 'Value must be between 1 to 180',
131+
error: 'Value must be between 0 to 195',
128132
readOnly: true,
129133
type: 'number',
130-
value: 0,
134+
value: 200,
131135
},
132136
{}
133137
)
@@ -153,7 +157,7 @@ describe('AirGap', () => {
153157
expect(vi.mocked(InputField)).toHaveBeenCalledWith(
154158
{
155159
title: 'Air gap volume (µL)',
156-
error: 'Value must be between 1 to 80',
160+
error: null,
157161
readOnly: true,
158162
type: 'number',
159163
value: 0,
@@ -180,7 +184,7 @@ describe('AirGap', () => {
180184
expect(vi.mocked(InputField)).toHaveBeenCalledWith(
181185
{
182186
title: 'Air gap volume (µL)',
183-
error: 'Value must be between 1 to 140',
187+
error: null,
184188
readOnly: true,
185189
type: 'number',
186190
value: 0,
@@ -205,7 +209,7 @@ describe('AirGap', () => {
205209
expect(vi.mocked(InputField)).toHaveBeenCalledWith(
206210
{
207211
title: 'Air gap volume (µL)',
208-
error: 'Value must be between 1 to 200',
212+
error: null,
209213
readOnly: true,
210214
type: 'number',
211215
value: 0,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getTiprackVolume } from '@opentrons/shared-data'
2+
3+
import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data'
4+
5+
interface VolumeRange {
6+
min: number
7+
max: number
8+
}
9+
10+
export const getAspirateAirGapVolumeRange = (
11+
pipette: PipetteV2Specs,
12+
tipRack: LabwareDefinition2
13+
): VolumeRange => {
14+
const minPipetteVolume = Object.values(pipette.liquids)[0].minVolume
15+
const maxPipetteVolume = Object.values(pipette.liquids)[0].maxVolume
16+
const minAirGapVolume = 0
17+
const tipRackTipVol = getTiprackVolume(tipRack)
18+
19+
const maxAirGapVolume =
20+
Math.min(maxPipetteVolume, tipRackTipVol) - minPipetteVolume
21+
22+
return {
23+
min: minAirGapVolume,
24+
max: maxAirGapVolume,
25+
}
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getTiprackVolume } from '@opentrons/shared-data'
2+
3+
import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data'
4+
import type { PathOption } from '../types'
5+
6+
interface VolumeRange {
7+
min: number
8+
max: number
9+
}
10+
11+
export const getDispenseAirGapVolumeRange = (
12+
volume: number,
13+
disposalVolume: number,
14+
path: PathOption,
15+
pipette: PipetteV2Specs,
16+
tipRack: LabwareDefinition2
17+
): VolumeRange => {
18+
const maxPipetteVolume = Object.values(pipette.liquids)[0].maxVolume
19+
const minAirGapVolume = 0
20+
const tipRackTipVol = getTiprackVolume(tipRack)
21+
const capacity = Math.min(maxPipetteVolume, tipRackTipVol)
22+
const maxAirGapVolume =
23+
path === 'multiDispense' ? capacity - disposalVolume - volume : capacity
24+
return {
25+
min: minAirGapVolume,
26+
max: maxAirGapVolume,
27+
}
28+
}

app/src/organisms/ODD/QuickTransferFlow/utils/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ export { checkLiquidClassCompatibility } from './checkLiquidClassCompatibility'
22
export { createQuickTransferFile } from './createQuickTransferFile'
33
export { generateCompatibleLabwareForPipette } from './generateCompatibleLabwareForPipette'
44
export { generateQuickTransferArgs } from './generateQuickTransferArgs'
5+
export { getAspirateAirGapVolumeRange } from './getAspirateAirGapVolumeRange'
56
export { getCompatibleLabwareByCategory } from './getCompatibleLabwareByCategory'
7+
export { getDispenseAirGapVolumeRange } from './getDispenseAirGapVolumeRange'
68
export { getInitialSummaryState } from './getInitialSummaryState'
79
export { getMaxConditioningVolume } from './getMaxConditioningVolume'
810
export { getMaxUiFlowRate } from './getMaxUiFlowRate'

0 commit comments

Comments
 (0)