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(app, shared-data, step-generation): frontend low volume support #13526

Merged
merged 13 commits into from
Sep 13, 2023
Merged
2 changes: 2 additions & 0 deletions app/src/assets/localization/en/protocol_command_text.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
"blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
"configure_for_volume": "Configure {{pipette}} to aspirate {{volume}} µL",
"closing_tc_lid": "Closing Thermocycler lid",
"comment": "Comment",
"confirm_and_resume": "Confirm and resume",
Expand All @@ -12,6 +13,7 @@
"degrees_c": "{{temp}}°C",
"disengaging_magnetic_module": "Disengaging Magnetic Module",
"dispense": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
"dispense_push_out": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec and pushing out {{push_out_volume}} µL",
"drop_tip": "Dropping tip in {{well_name}} of {{labware}}",
"engaging_magnetic_module": "Engaging Magnetic Module",
"fixed_trash": "Fixed Trash",
Expand Down
25 changes: 17 additions & 8 deletions app/src/organisms/CommandText/PipettingCommandText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,23 @@ export const PipettingCommandText = ({
})
}
case 'dispense': {
const { volume, flowRate } = command.params
return t('dispense', {
well_name: wellName,
labware: getLabwareName(robotSideAnalysis, labwareId),
labware_location: displayLocation,
volume: volume,
flow_rate: flowRate,
})
const { volume, flowRate, pushOut } = command.params
return pushOut
? t('dispense_push_out', {
well_name: wellName,
labware: getLabwareName(robotSideAnalysis, labwareId),
labware_location: displayLocation,
volume: volume,
flow_rate: flowRate,
push_out_volume: pushOut,
})
: t('dispense', {
well_name: wellName,
labware: getLabwareName(robotSideAnalysis, labwareId),
labware_location: displayLocation,
volume: volume,
flow_rate: flowRate,
})
}
case 'blowout': {
const { flowRate } = command.params
Expand Down
51 changes: 49 additions & 2 deletions app/src/organisms/CommandText/__tests__/CommandText.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { CommandText } from '../'
import { mockRobotSideAnalysis } from '../__fixtures__'

import type { MoveToWellRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command/gantry'
import type { BlowoutRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command/pipetting'
import type {
BlowoutRunTimeCommand,
DispenseRunTimeCommand,
ConfigureForVolumeRunTimeCommand,
} from '@opentrons/shared-data/protocol/types/schemaV7/command/pipetting'
import type {
LoadLabwareRunTimeCommand,
LoadLiquidRunTimeCommand,
Expand All @@ -31,7 +35,7 @@ describe('CommandText', () => {
)
}
})
it('renders correct text for dispense', () => {
it('renders correct text for dispense without pushOut', () => {
const command = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'dispense'
)
Expand All @@ -49,6 +53,31 @@ describe('CommandText', () => {
)
}
})
it('renders correct text for dispense with pushOut', () => {
const command = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'dispense'
)
const pushOutDispenseCommand = {
...command,
params: {
...command?.params,
pushOut: 10,
},
} as DispenseRunTimeCommand
expect(pushOutDispenseCommand).not.toBeUndefined()
if (pushOutDispenseCommand != null) {
const { getByText } = renderWithProviders(
<CommandText
robotSideAnalysis={mockRobotSideAnalysis}
command={pushOutDispenseCommand}
/>,
{ i18nInstance: i18n }
)[0]
getByText(
'Dispensing 100 µL into well A1 of NEST 96 Well Plate 100 µL PCR Full Skirt (1) in Magnetic Module GEN2 in Slot 1 at 300 µL/sec and pushing out 10 µL'
)
}
})
it('renders correct text for blowout', () => {
const dispenseCommand = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'dispense'
Expand Down Expand Up @@ -91,6 +120,24 @@ describe('CommandText', () => {
getByText('Moving to well A1 of NEST 1 Well Reservoir 195 mL in Slot 5')
}
})
it('renders correct text for configureForVolume', () => {
const command = {
commandType: 'configureForVolume',
params: {
volume: 1,
pipetteId: 'f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb',
},
} as ConfigureForVolumeRunTimeCommand

const { getByText } = renderWithProviders(
<CommandText
robotSideAnalysis={mockRobotSideAnalysis}
command={command}
/>,
{ i18nInstance: i18n }
)[0]
getByText('Configure P300 Single-Channel GEN1 to aspirate 1 µL')
})
it('renders correct text for dropTip', () => {
const command = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'dropTip'
Expand Down
20 changes: 19 additions & 1 deletion app/src/organisms/CommandText/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'

import { Flex, DIRECTION_COLUMN, SPACING } from '@opentrons/components'
import { getPipetteNameSpecs, RunTimeCommand } from '@opentrons/shared-data'
import { StyledText } from '../../atoms/text'
import { LoadCommandText } from './LoadCommandText'
import { PipettingCommandText } from './PipettingCommandText'
import { TemperatureCommandText } from './TemperatureCommandText'
import { MoveLabwareCommandText } from './MoveLabwareCommandText'

import type { RunTimeCommand } from '@opentrons/shared-data'
import type { CompletedProtocolAnalysis } from '@opentrons/shared-data/js'
import type { StyleProps } from '@opentrons/components'

Expand Down Expand Up @@ -139,6 +139,24 @@ export function CommandText(props: Props): JSX.Element | null {
</StyledText>
)
}
case 'configureForVolume': {
const { volume, pipetteId } = command.params
const pipetteName = robotSideAnalysis.pipettes.find(
pip => pip.id === pipetteId
)?.pipetteName

return (
<StyledText as="p" {...styleProps}>
{t('configure_for_volume', {
volume,
pipette:
pipetteName != null
? getPipetteNameSpecs(pipetteName)?.displayName
: '',
})}
</StyledText>
)
}
case 'touchTip':
case 'home':
case 'savePosition':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,16 @@ def _pipette_with_liquid_settings(
# FIXME: stop using hwapi, and get those functions into core software
hw_api = ctx._core.get_hardware()
hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT
hw_pipette = hw_api.hardware_pipettes[hw_mount.to_mount()]
_check_aspirate_dispense_args(aspirate, dispense)

def _dispense_with_added_blow_out() -> None:
# dispense all liquid, plus some air by calling `pipette.blow_out(location, volume)`
# FIXME: this is a hack, until there's an equivalent `pipette.blow_out(location, volume)`
# dispense all liquid, plus some air
# FIXME: push-out is not supported in Legacy core, so here
# we again use the hardware controller
hw_api = ctx._core.get_hardware()
hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT
hw_api.blow_out(hw_mount, liquid_class.dispense.blow_out_submerged)
hw_api.dispense(hw_mount, push_out=liquid_class.dispense.blow_out_submerged)

# ASPIRATE/DISPENSE SEQUENCE HAS THREE PHASES:
# 1. APPROACH
Expand Down Expand Up @@ -211,14 +213,18 @@ def _dispense_with_added_blow_out() -> None:

# CREATE CALLBACKS FOR EACH PHASE
def _aspirate_on_approach() -> None:
if hw_pipette.current_volume > 0:
print(
"WARNING: removing trailing air-gap from pipette, "
"this should only happen during blank trials"
)
hw_api.dispense(hw_mount)
hw_api.configure_for_volume(hw_mount, aspirate if aspirate else dispense)
hw_api.prepare_for_aspirate(hw_mount)
if liquid_class.aspirate.leading_air_gap > 0:
pipette.aspirate(liquid_class.aspirate.leading_air_gap)

def _aspirate_on_submerge() -> None:
# TODO: re-implement mixing once we have a real use for it
# and once the rest of the script settles down
if mix:
raise NotImplementedError("mixing is not currently implemented")
# aspirate specified volume
callbacks.on_aspirating()
pipette.aspirate(aspirate)
Expand Down Expand Up @@ -308,8 +314,9 @@ def aspirate_with_liquid_class(
touch_tip: bool = False,
) -> None:
"""Aspirate with liquid class."""
pip_size = 50 if "50" in pipette.name else 1000
liquid_class = get_liquid_class(
int(pipette.max_volume), pipette.channels, tip_volume, int(aspirate_volume)
pip_size, pipette.channels, tip_volume, int(aspirate_volume)
)
_pipette_with_liquid_settings(
ctx,
Expand Down Expand Up @@ -345,8 +352,9 @@ def dispense_with_liquid_class(
touch_tip: bool = False,
) -> None:
"""Dispense with liquid class."""
pip_size = 50 if "50" in pipette.name else 1000
liquid_class = get_liquid_class(
int(pipette.max_volume), pipette.channels, tip_volume, int(dispense_volume)
pip_size, pipette.channels, tip_volume, int(dispense_volume)
)
_pipette_with_liquid_settings(
ctx,
Expand Down
22 changes: 21 additions & 1 deletion shared-data/protocol/types/schemaV7/command/pipetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,32 @@ export type PipettingRunTimeCommand =
| TouchTipRunTimeCommand
| PickUpTipRunTimeCommand
| DropTipRunTimeCommand
| ConfigureForVolumeRunTimeCommand

export type PipettingCreateCommand =
| AspirateCreateCommand
| DispenseCreateCommand
| BlowoutCreateCommand
| TouchTipCreateCommand
| PickUpTipCreateCommand
| DropTipCreateCommand
| ConfigureForVolumeCreateCommand

export interface ConfigureForVolumeCreateCommand
extends CommonCommandCreateInfo {
commandType: 'configureForVolume'
params: ConfigureForVolumeParams
}

export interface ConfigureForVolumeParams {
pipetteId: string
volume: number
}
export interface ConfigureForVolumeRunTimeCommand
extends CommonCommandRunTimeInfo,
ConfigureForVolumeCreateCommand {
result?: BasicLiquidHandlingResult
}
export interface AspirateCreateCommand extends CommonCommandCreateInfo {
commandType: 'aspirate'
params: AspDispAirgapParams
Expand All @@ -23,9 +41,11 @@ export interface AspirateRunTimeCommand
AspirateCreateCommand {
result?: BasicLiquidHandlingResult
}

export type DispenseParams = AspDispAirgapParams & { pushOut?: number }
export interface DispenseCreateCommand extends CommonCommandCreateInfo {
commandType: 'dispense'
params: AspDispAirgapParams
params: DispenseParams
}
export interface DispenseRunTimeCommand
extends CommonCommandRunTimeInfo,
Expand Down
38 changes: 38 additions & 0 deletions step-generation/src/__tests__/configureForVolume.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getSuccessResult } from '../fixtures'
import { configureForVolume } from '../commandCreators/atomic/configureForVolume'

const getRobotInitialState = (): any => {
return {}
}
const mockId = 'mockId'
const invariantContext: any = {
pipetteEntities: {
[mockId]: {
name: 'p50_single_flex',
id: mockId,
},
},
}

describe('configureForVolume', () => {
it('should call configureForVolume with correct params', () => {
const robotInitialState = getRobotInitialState()
const mockId = 'mockId'
const result = configureForVolume(
{ pipetteId: mockId, volume: 1 },
invariantContext,
robotInitialState
)
const res = getSuccessResult(result)
expect(res.commands).toEqual([
{
commandType: 'configureForVolume',
key: expect.any(String),
params: {
pipetteId: mockId,
volume: 1,
},
},
])
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { uuid } from '../../utils'
import type { CommandCreator } from '../../types'
interface configureForVolumeArgs {
pipetteId: string
volume: number
}

export const configureForVolume: CommandCreator<configureForVolumeArgs> = (
args,
invariantContext,
prevRobotState
) => {
const { pipetteId, volume } = args

// No-op if there is no pipette
if (!invariantContext.pipetteEntities[pipetteId]) {
return {

Check warning on line 17 in step-generation/src/commandCreators/atomic/configureForVolume.ts

View check run for this annotation

Codecov / codecov/patch

step-generation/src/commandCreators/atomic/configureForVolume.ts#L17

Added line #L17 was not covered by tests
commands: [],
}
}

const commands = [
{
commandType: 'configureForVolume' as const,
key: uuid(),
params: {
pipetteId,
volume,
},
},
]
return {
commands,
}
}
2 changes: 2 additions & 0 deletions step-generation/src/commandCreators/atomic/dispense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ export const dispense: CommandCreator<DispenseParams> = (
},
},
flowRate,
// pushOut will always be undefined in step-generation for now
// since there is no easy way to allow users to select a volume for it in PD
},
...(isAirGap && { meta: { isAirGap } }),
},
Expand Down
14 changes: 14 additions & 0 deletions step-generation/src/commandCreators/compound/mix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
CurriedCommandCreator,
} from '../../types'
import { aspirate, dispense, delay, replaceTip, touchTip } from '../atomic'
import { configureForVolume } from '../atomic/configureForVolume'

/** Helper fn to make mix command creators w/ minimal arguments */
export function mixUtil(args: {
Expand Down Expand Up @@ -137,6 +138,18 @@ export const mix: CommandCreator<MixArgs> = (
}
}

const configureForVolumeCommand: CurriedCommandCreator[] =
volume <= 1 &&
(invariantContext.pipetteEntities[pipette].name === 'p50_single_flex' ||
invariantContext.pipetteEntities[pipette].name === 'p50_multi_flex')
? [
curryCommandCreator(configureForVolume, {
pipetteId: pipette,
volume: volume,
}),
]
: []

// Command generation
const commandCreators = flatMap(
wells,
Expand Down Expand Up @@ -187,6 +200,7 @@ export const mix: CommandCreator<MixArgs> = (
})
return [
...tipCommands,
...configureForVolumeCommand,
...mixCommands,
...blowoutCommand,
...touchTipCommands,
Expand Down
Loading
Loading