Skip to content

Commit

Permalink
fix(protocol-designer): do not add __air__ on blowout
Browse files Browse the repository at this point in the history
Closes #2498
  • Loading branch information
IanLondon committed Oct 23, 2018
1 parent 7bf6c92 commit a58110d
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 34 deletions.
2 changes: 1 addition & 1 deletion protocol-designer/src/step-generation/blowout.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const blowout = (args: PipetteLabwareFields): CommandCreator => (prevRobotState:
pipetteData,
labwareId: labware,
labwareType: prevRobotState.labware[labware].type,
volume: pipetteData.maxVolume, // update liquid state as if it was a dispense, but with max volume of pipette
useFullVolume: true,
well,
}, prevRobotState.liquidState),
},
Expand Down
36 changes: 24 additions & 12 deletions protocol-designer/src/step-generation/dispenseUpdateLiquidState.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,53 @@
// @flow
import assert from 'assert'
import cloneDeep from 'lodash/cloneDeep'
import mapValues from 'lodash/mapValues'
import reduce from 'lodash/reduce'
import {splitLiquid, mergeLiquid, getWellsForTips} from './utils'
import type {RobotState, LocationLiquidState, PipetteData} from './'
import {splitLiquid, mergeLiquid, getWellsForTips, getLocationTotalVolume} from './utils'
import type {RobotState, LocationLiquidState, PipetteData, SourceAndDest} from './types'

type LiquidState = $PropertyType<RobotState, 'liquidState'>

export default function updateLiquidState (
args: {
pipetteId: string,
pipetteData: PipetteData,
volume: number,
volume?: number,
useFullVolume?: boolean,
labwareId: string,
labwareType: string,
well: string,
},
prevLiquidState: LiquidState
): LiquidState {
// TODO: Ian 2018-06-14 return same shape as aspirateUpdateLiquidState fn: {liquidState, warnings}.
const {pipetteId, pipetteData, volume, labwareId, labwareType, well} = args
type SourceAndDest = {|source: LocationLiquidState, dest: LocationLiquidState|}
const {pipetteId, pipetteData, volume, useFullVolume, labwareId, labwareType, well} = args

assert(
!(useFullVolume && typeof volume === 'number'),
'dispenseUpdateLiquidState takes either `volume` or `useFullVolume`, but got both')
assert(typeof volume === 'number' || useFullVolume,
'in dispenseUpdateLiquidState, either volume or useFullVolume are required')

const {wellsForTips, allWellsShared} = getWellsForTips(
pipetteData.channels, labwareType, well)

// remove liquid from pipette tips,
// create intermediate object where sources are updated tip liquid states
// and dests are "droplets" that need to be merged to dest well contents
const splitLiquidStates: {[tipId: string]: SourceAndDest} = mapValues(
prevLiquidState.pipettes[pipetteId],
(prevTipLiquidState: LocationLiquidState) =>
splitLiquid(
volume,
prevTipLiquidState
)
(prevTipLiquidState: LocationLiquidState): SourceAndDest => {
if (useFullVolume) {
const totalTipVolume = getLocationTotalVolume(prevTipLiquidState)
return totalTipVolume > 0
? splitLiquid(totalTipVolume, prevTipLiquidState)
: {source: {}, dest: {}}
}
return splitLiquid(volume || 0, prevTipLiquidState)
}
)

const {wellsForTips, allWellsShared} = getWellsForTips(pipetteData.channels, labwareType, well)

// add liquid to well(s)
const labwareLiquidState = allWellsShared
// merge all liquid into the single well
Expand Down
2 changes: 1 addition & 1 deletion protocol-designer/src/step-generation/dropTip.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const dropTip = (pipetteId: string): CommandCreator => (prevRobotState: RobotSta
pipetteData: prevRobotState.instruments[pipetteId],
labwareId: FIXED_TRASH_ID,
labwareType: 'fixed-trash',
volume: prevRobotState.instruments[pipetteId].maxVolume, // update liquid state as if it was a dispense, but with max volume of pipette
useFullVolume: true,
well: 'A1',
}, prevRobotState.liquidState),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe('blowout', () => {
expect(updateLiquidState).toHaveBeenCalledWith({
pipetteId: 'p300SingleId',
labwareId: 'sourcePlateId',
volume: 300, // pipette's max vol
useFullVolume: true,
well: 'A1',
labwareType: 'trough-12row',
pipetteData: robotStateWithTip.instruments.p300SingleId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow
import merge from 'lodash/merge'
import omit from 'lodash/omit'
import {
createEmptyLiquidState,
createTipLiquidState,
Expand Down Expand Up @@ -35,7 +36,7 @@ beforeEach(() => {
})

describe('...single-channel pipette', () => {
test('fully dispense single ingredient into empty well', () => {
test('fully dispense single ingredient into empty well, with explicit volume', () => {
const initialLiquidState = merge(
{},
getBlankLiquidState(),
Expand Down Expand Up @@ -73,6 +74,47 @@ describe('...single-channel pipette', () => {
})
})

test('fully dispense single ingredient into empty well, with useFullVolume', () => {
const initialLiquidState = merge(
{},
getBlankLiquidState(),
{
pipettes: {
p300SingleId: {
'0': {
ingred1: {volume: 150},
},
},
},
}
)

const result = _updateLiquidState(
{
...omit(dispenseSingleCh150ToA1Args, 'volume'),
useFullVolume: true,
},
initialLiquidState
)

expect(result).toMatchObject({
pipettes: {
p300SingleId: {
'0': {
ingred1: {volume: 0},
},
},
},
labware: {
sourcePlateId: {
A1: {ingred1: {volume: 150}},
A2: {},
B1: {},
},
},
})
})

test('dispense ingred 1 into well containing ingreds 1 & 2', () => {
const initialLiquidState = merge(
{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe('dropTip', () => {
updateLiquidState.mockReturnValue(mockLiquidReturnValue)
})

test('dropTip calls dispenseUpdateLiquidState with the max volume of the pipette', () => {
test('dropTip calls dispenseUpdateLiquidState with useFullVolume: true', () => {
const initialRobotState = makeRobotState({singleHasTips: true, multiHasTips: true})

const result = dropTip('p300MultiId')(initialRobotState)
Expand All @@ -126,7 +126,7 @@ describe('dropTip', () => {
{
pipetteId: 'p300MultiId',
labwareId: 'trashId',
volume: 300, // pipette's max vol
useFullVolume: true,
well: 'A1',
labwareType: 'fixed-trash',
pipetteData: robotStateWithTip.instruments.p300MultiId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
commandFixtures as cmd,
} from './fixtures'
import _transfer from '../transfer'
import {FIXED_TRASH_ID} from '../../constants'

const transfer = compoundCommandCreatorNoErrors(_transfer)
const transferWithErrors = compoundCommandCreatorHasErrors(_transfer)
Expand Down Expand Up @@ -256,9 +255,14 @@ describe('single transfer exceeding pipette max', () => {
cmd.dispense('B3', 50, {labware: 'destPlateId'}),
])

// ignore trash contents because of "__air__" from dropped tips
// TODO: Ian 2018-09-17 fix "__air__" and remove this line $FlowFixMe
expectedFinalLiquidState.labware[FIXED_TRASH_ID] = result.robotState.liquidState.labware[FIXED_TRASH_ID]
// unlike the other test cases here, we have a new tip when aspirating from B1.
// so there's only ingred 1, and no ingred 0
// $FlowFixMe flow doesn't like assigning to these objects
expectedFinalLiquidState.pipettes.p300SingleId['0'] = {'1': {volume: 0}}

// likewise, there's no residue of ingred 0 in B3 from a dirty tip.
// $FlowFixMe flow doesn't like assigning to these objects
expectedFinalLiquidState.labware.destPlateId.B3 = {'1': {volume: 350}}

expect(result.robotState.liquidState).toEqual(merge(
{},
Expand Down
2 changes: 2 additions & 0 deletions protocol-designer/src/step-generation/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ export type SingleLabwareLiquidState = {[well: string]: LocationLiquidState}

export type LabwareLiquidState = {[labwareId: string]: SingleLabwareLiquidState}

export type SourceAndDest = {|source: LocationLiquidState, dest: LocationLiquidState|}

// TODO Ian 2018-02-09 Rename this so it's less ambigious with what we call "robot state": RobotSimulationState?
export type RobotState = {|
instruments: { // TODO Ian 2018-05-23 rename this 'pipettes' to match tipState (& to disambiguate from future 'modules')
Expand Down
27 changes: 15 additions & 12 deletions protocol-designer/src/step-generation/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import last from 'lodash/last'
import {computeWellAccess} from '@opentrons/shared-data'
import type {
CommandCreator,
LocationLiquidState,
RobotState,
SourceAndDest,
Timeline,
LocationLiquidState,
} from './types'

import {AIR} from '@opentrons/components'
Expand Down Expand Up @@ -91,13 +92,9 @@ export const commandCreatorsTimeline = (commandCreators: Array<CommandCreator>)

type Vol = {volume: number}

/** Breaks a liquid volume state into 2 parts. Assumes all liquids are evenly mixed. */
export function splitLiquid (volume: number, sourceLiquidState: LocationLiquidState): {
source: LocationLiquidState,
dest: LocationLiquidState,
} {
const totalSourceVolume = reduce(
sourceLiquidState,
export function getLocationTotalVolume (loc: LocationLiquidState): number {
return reduce(
loc,
(acc: number, ingredState: Vol, ingredId: string) => {
// air is not included in the total volume
return (ingredId === AIR)
Expand All @@ -106,13 +103,18 @@ export function splitLiquid (volume: number, sourceLiquidState: LocationLiquidSt
},
0
)
}

/** Breaks a liquid volume state into 2 parts. Assumes all liquids are evenly mixed. */
export function splitLiquid (volume: number, sourceLiquidState: LocationLiquidState): SourceAndDest {
const totalSourceVolume = getLocationTotalVolume(sourceLiquidState)

// TODO Ian 2018-03-19 figure out what to do with air warning reporting
// if (AIR in sourceLiquidState) {
// console.warn('Splitting liquid with air present', sourceLiquidState)
// }
if (AIR in sourceLiquidState) {
console.warn('Splitting liquid with air present', sourceLiquidState)
}

if (totalSourceVolume === 0) {
console.warn('splitting with zero source volume')
// Splitting from empty source
return {
source: sourceLiquidState,
Expand All @@ -121,6 +123,7 @@ export function splitLiquid (volume: number, sourceLiquidState: LocationLiquidSt
}

if (volume > totalSourceVolume) {
console.warn('volume to split exceeds total source volume, adding air', sourceLiquidState, volume, totalSourceVolume)
// Take all of source, plus air
return {
source: mapValues(sourceLiquidState, () => ({volume: 0})),
Expand Down

0 comments on commit a58110d

Please sign in to comment.