Skip to content

Commit

Permalink
fix(app): fix tip capacity calculation for pipette pathing (#16085)
Browse files Browse the repository at this point in the history
  • Loading branch information
smb2268 authored Aug 21, 2024
1 parent a340cc5 commit 4267278
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 33 deletions.
4 changes: 4 additions & 0 deletions app/src/assets/localization/en/quick_transfer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"air_gap": "Air gap",
"air_gap_before_aspirating": "Air gap before aspirating",
"air_gap_before_dispensing": "Air gap before dispensing",
"air_gap_capacity_error": "The tip is too full to add an air gap.",
"air_gap_value": "{{volume}} µL",
"air_gap_volume_µL": "Air gap volume (µL)",
"all": "All labware",
Expand All @@ -33,6 +34,8 @@
"character_limit_error": "Character limit exceeded",
"column": "column",
"columns": "columns",
"consolidate_volume_error": "The selected destination well is too small to consolidate into. Try consolidating from fewer wells.",
"create_new_to_edit": "Create a new quick transfer to edit",
"create_new_transfer": "Create new quick transfer",
"create_transfer": "Create transfer",
"delay": "Delay",
Expand All @@ -56,6 +59,7 @@
"dispense_volume_µL": "Dispense volume per well (µL)",
"disposal_volume_µL": "Disposal volume (µL)",
"distance_bottom_of_well_mm": "Distance from bottom of well (mm)",
"distribute_volume_error": "The selected source well is too small to distribute from. Try distributing to fewer wells.",
"enter_characters": "Enter up to 60 characters",
"error_analyzing": "<block>An error occurred while attempting to analyze <bold>{{transferName}}.</bold></block>",
"exit_quick_transfer": "Exit quick transfer?",
Expand Down
7 changes: 6 additions & 1 deletion app/src/organisms/QuickTransferFlow/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
COLORS,
TEXT_ALIGN_RIGHT,
} from '@opentrons/components'
import { useToaster } from '../ToasterOven'
import { ListItem } from '../../atoms/ListItem'
import { CONSOLIDATE, DISTRIBUTE } from './constants'

Expand All @@ -22,13 +23,17 @@ interface OverviewProps {
export function Overview(props: OverviewProps): JSX.Element | null {
const { state } = props
const { t } = useTranslation(['quick_transfer', 'shared'])
const { makeSnackbar } = useToaster()

let transferCopy = t('volume_per_well')
if (state.transferType === CONSOLIDATE) {
transferCopy = t('aspirate_volume')
} else if (state.transferType === DISTRIBUTE) {
transferCopy = t('dispense_volume')
}
const onClick = (): void => {
makeSnackbar(t('create_new_to_edit') as string)
}

const displayItems = [
{
Expand Down Expand Up @@ -63,7 +68,7 @@ export function Overview(props: OverviewProps): JSX.Element | null {
marginTop="192px"
>
{displayItems.map(displayItem => (
<ListItem type="noActive" key={displayItem.option}>
<ListItem type="noActive" key={displayItem.option} onClick={onClick}>
<Flex justifyContent={JUSTIFY_SPACE_BETWEEN} width="100%">
<LegacyStyledText
css={TYPOGRAPHY.level4HeaderSemiBold}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,18 @@ export function AirGap(props: AirGapProps): JSX.Element {
}

const volumeRange = { min: 1, max: Math.floor(maxAvailableCapacity) }
const volumeError =
volume !== null && (volume < volumeRange.min || volume > volumeRange.max)
? t(`value_out_of_range`, {
min: volumeRange.min,
max: volumeRange.max,
})
: null
let volumeError = null
if (volumeRange.min > volumeRange.max) {
volumeError = t('air_gap_capacity_error')
} else if (
volume !== null &&
(volume < volumeRange.min || volume > volumeRange.max)
) {
volumeError = t(`value_out_of_range`, {
min: volumeRange.min,
max: volumeRange.max,
})
}

let buttonIsDisabled = false
if (currentStep === 2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { getTopPortalEl } from '../../../App/portal'
import { LargeButton } from '../../../atoms/buttons'
import { ChildNavigation } from '../../ChildNavigation'
import { useBlowOutLocationOptions } from './BlowOut'
import { getVolumeRange } from '../utils'

import type {
PathOption,
Expand Down Expand Up @@ -48,15 +47,19 @@ export function PipettePath(props: PipettePathProps): JSX.Element {
const [disposalVolume, setDisposalVolume] = React.useState<number>(
state.volume
)
const volumeLimits = getVolumeRange(state)
const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume
const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume

// this is the max amount of liquid that can be held in the tip at any time
const maxTipCapacity = Math.min(maxPipetteVolume, tipVolume)

const allowedPipettePathOptions: Array<{
pathOption: PathOption
description: string
}> = [{ pathOption: 'single', description: t('pipette_path_single') }]
if (
state.transferType === 'distribute' &&
volumeLimits.max >= state.volume * 3
maxTipCapacity >= state.volume * 3
) {
// we have the capacity for a multi dispense if we can fit at least 2x the volume per well
// for aspiration plus 1x the volume per well for disposal volume
Expand All @@ -67,7 +70,7 @@ export function PipettePath(props: PipettePathProps): JSX.Element {
// for multi aspirate we only need at least 2x the volume per well
} else if (
state.transferType === 'consolidate' &&
volumeLimits.max >= state.volume * 2
maxTipCapacity >= state.volume * 2
) {
allowedPipettePathOptions.push({
pathOption: 'multiAspirate',
Expand Down Expand Up @@ -113,8 +116,8 @@ export function PipettePath(props: PipettePathProps): JSX.Element {
? t('shared:continue')
: t('shared:save')

const maxVolumeCapacity = volumeLimits.max - state.volume * 2
const volumeRange = { min: 1, max: maxVolumeCapacity }
const maxDisposalCapacity = maxTipCapacity - state.volume * 2
const volumeRange = { min: 1, max: maxDisposalCapacity }

const volumeError =
disposalVolume !== null &&
Expand Down
20 changes: 13 additions & 7 deletions app/src/organisms/QuickTransferFlow/VolumeEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,21 @@ export function VolumeEntry(props: VolumeEntryProps): JSX.Element {
onNext()
}
}

const error =
let error = null
if (volumeRange.min > volumeRange.max) {
error =
state.transferType === 'consolidate'
? t('consolidate_volume_error')
: t('distribute_volume_error')
} else if (
volume !== '' &&
(volumeAsNumber < volumeRange.min || volumeAsNumber > volumeRange.max)
? t(`value_out_of_range`, {
min: volumeRange.min,
max: volumeRange.max,
})
: null
) {
error = t(`value_out_of_range`, {
min: volumeRange.min,
max: volumeRange.max,
})
}

return (
<Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { describe, it, expect, vi, afterEach } from 'vitest'
import { getInitialSummaryState } from '../../utils'
import { getVolumeRange } from '../../utils/getVolumeRange'

vi.mock('../../utils/getVolumeRange')

describe('getInitialSummaryState', () => {
const props = {
Expand All @@ -11,6 +8,7 @@ describe('getInitialSummaryState', () => {
channels: 1,
liquids: {
default: {
maxVolume: 100,
supportedTips: {
t50: {
defaultAspirateFlowRate: {
Expand Down Expand Up @@ -46,9 +44,6 @@ describe('getInitialSummaryState', () => {
},
],
} as any
beforeEach(() => {
vi.mocked(getVolumeRange).mockReturnValue({ min: 5, max: 100 })
})
afterEach(() => {
vi.resetAllMocks()
})
Expand Down Expand Up @@ -125,11 +120,13 @@ describe('getInitialSummaryState', () => {
...props,
state: {
...props.state,
volume: 10,
transferType: 'distribute',
},
})
expect(initialSummaryState).toEqual({
...props.state,
volume: 10,
transferType: 'distribute',
aspirateFlowRate: 50,
dispenseFlowRate: 75,
Expand All @@ -142,7 +139,7 @@ describe('getInitialSummaryState', () => {
cutoutId: 'cutoutA3',
cutoutFixtureId: 'trashBinAdapter',
},
disposalVolume: props.state.volume,
disposalVolume: 10,
blowOut: { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter' },
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
TRASH_BIN_ADAPTER_FIXTURE,
WASTE_CHUTE_FIXTURES,
} from '@opentrons/shared-data'
import { getVolumeRange } from './'

import type {
LabwareDefinition2,
Expand Down Expand Up @@ -43,20 +42,24 @@ export function getInitialSummaryState(
const flowRatesForSupportedTip =
state.pipette.liquids.default.supportedTips[tipType]

const volumeLimits = getVolumeRange(state)
const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume
const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume

// this is the max amount of liquid that can be held in the tip at any time
const maxTipCapacity = Math.min(maxPipetteVolume, tipVolume)

let path: PathOption = 'single'
// for multiDispense the volume capacity must be at least 3x the volume per well
// to account for the 1x volume per well disposal volume default
if (
state.transferType === 'distribute' &&
volumeLimits.max >= state.volume * 3
maxTipCapacity >= state.volume * 3
) {
path = 'multiDispense'
// for multiAspirate the volume capacity must be at least 2x the volume per well
} else if (
state.transferType === 'consolidate' &&
volumeLimits.max >= state.volume * 2
maxTipCapacity >= state.volume * 2
) {
path = 'multiAspirate'
}
Expand Down

0 comments on commit 4267278

Please sign in to comment.