From 40f3a9eda56156f6db0acbbd423f41d98cac0c73 Mon Sep 17 00:00:00 2001 From: Ian London Date: Tue, 18 Jun 2019 13:47:50 -0400 Subject: [PATCH] feat(protocol-designer): save v3 protocols (#3588) Closes #3336 and closes #3414 - change command creators to match v3 protocol schema - deep refactor of test fixtures, too many hard-coded strings etc, to allow testing flow rates and offsets somewhat sanely - remove vestigal `defaultValues` access in APIv1 executor for JSON v3 protocols - minor changes to v3 protocol schema - for consolidate and transfer, make touch tip happen after the destination well's inner mix, do not touch tip between the dispense and the inner mix - redo shared-data schemaV1/schemaV3 imports - `import {NameOfType} from '@opentrons/shared-data/path/to/schemaV3` vs `import {NameOfTypeV3} from '@opentrons/shared-data'` --- api/src/opentrons/protocols/execute_v3.py | 58 +-- .../opentrons/protocols/test_execute_v3.py | 35 +- app/src/protocol/types.js | 6 +- .../FileSidebar/ConnectedFileSidebar.js | 2 +- .../fields/TipPosition/TipPositionModal.js | 18 +- .../fields/TipPosition/TipPositionZAxisViz.js | 10 +- .../StepEditForm/fields/TipPosition/index.js | 19 +- .../StepEditForm/fields/TipPosition/utils.js | 6 +- .../FilePipettesModal/TiprackDiagram.js | 2 +- .../src/components/steplist/PauseStepItems.js | 4 +- protocol-designer/src/constants.js | 4 +- .../src/file-data/reducers/index.js | 6 +- .../src/file-data/selectors/fileCreator.js | 101 ++-- .../src/file-data/selectors/fileFields.js | 4 +- protocol-designer/src/file-data/types.js | 7 +- protocol-designer/src/file-types.js | 17 +- protocol-designer/src/form-types.js | 28 +- .../src/labware-defs/selectors.js | 2 +- .../src/labware-ingred/reducers/index.js | 5 +- protocol-designer/src/labware-ingred/types.js | 2 +- .../src/load-file/migration/1_1_0.js | 41 +- .../src/load-file/migration/index.js | 3 +- .../src/step-forms/reducers/index.js | 7 +- .../src/step-forms/selectors/index.js | 4 +- .../commandCreators/atomic/aspirate.js | 10 +- .../commandCreators/atomic/blowout.js | 8 +- .../commandCreators/atomic/delay.js | 10 +- .../commandCreators/atomic/dispense.js | 12 +- .../commandCreators/atomic/dropAllTips.js | 2 +- .../commandCreators/atomic/dropTip.js | 2 +- .../commandCreators/atomic/replaceTip.js | 2 +- .../commandCreators/atomic/touchTip.js | 9 +- .../commandCreators/compound/consolidate.js | 29 +- .../commandCreators/compound/distribute.js | 13 +- .../commandCreators/compound/mix.js | 33 +- .../commandCreators/compound/transfer.js | 27 +- .../forAspirateDispense.js | 7 +- .../getNextRobotStateAndWarnings/index.js | 12 +- .../test-with-flow/aspirate.test.js | 145 +++--- .../test-with-flow/blowout.test.js | 79 ++- .../test-with-flow/blowoutUtil.test.js | 79 ++- .../test-with-flow/consolidate.test.js | 469 ++++++++--------- .../test-with-flow/delay.test.js | 26 +- .../test-with-flow/dispense.test.js | 127 ++--- .../dispenseUpdateLiquidState.test.js | 10 +- .../test-with-flow/distribute.test.js | 324 ++++++------ .../test-with-flow/dropAllTips.test.js | 31 +- .../test-with-flow/dropTip.test.js | 49 +- .../fixtures/commandFixtures.js | 186 +++++-- .../test-with-flow/fixtures/index.js | 6 +- .../{fixtures.js => robotStateFixtures.js} | 88 +--- .../getNextRobotStateAndWarnings.test.js | 19 +- ...extRobotStateAndWarningsForAspDisp.test.js | 25 +- .../test-with-flow/mix.test.js | 310 +++++------- .../test-with-flow/replaceTip.test.js | 67 +-- .../robotStateSelectors.test.js | 13 +- .../test-with-flow/touchTip.test.js | 65 +-- .../test-with-flow/transfer.test.js | 471 +++++++++--------- .../test-with-flow/utils.test.js | 3 +- .../src/step-generation/types.js | 46 +- .../src/step-generation/utils.js | 76 ++- .../formLevel/stepFormToArgs/mixFormToArgs.js | 50 +- .../stepFormToArgs/moveLiquidFormToArgs.js | 138 ++--- .../stepFormToArgs/pauseFormToArgs.js | 6 +- .../test/moveLiquidFormToArgs.test.js | 47 +- .../src/steplist/generateSubsteps.js | 4 +- .../src/steplist/substepTimeline.js | 4 +- protocol-designer/src/steplist/types.js | 4 +- .../src/top-selectors/substep-highlight.js | 8 +- .../src/top-selectors/tip-contents/index.js | 9 +- protocol-designer/src/ui/steps/types.js | 4 +- protocol-designer/webpack.config.js | 2 +- shared-data/js/getLabware.js | 3 - shared-data/js/helpers/index.js | 27 +- shared-data/js/pipettes.js | 14 - shared-data/protocol/flowTypes/schemaV1.js | 26 +- shared-data/protocol/flowTypes/schemaV3.js | 91 ++-- shared-data/protocol/index.js | 7 +- shared-data/protocol/schemas/3.json | 5 +- 79 files changed, 1929 insertions(+), 1801 deletions(-) rename protocol-designer/src/step-generation/test-with-flow/fixtures/{fixtures.js => robotStateFixtures.js} (73%) diff --git a/api/src/opentrons/protocols/execute_v3.py b/api/src/opentrons/protocols/execute_v3.py index ed49cb50c5c..6b2b8a35d87 100644 --- a/api/src/opentrons/protocols/execute_v3.py +++ b/api/src/opentrons/protocols/execute_v3.py @@ -29,7 +29,7 @@ def load_labware(protocol_data): for labware_id, props in data.items(): slot = props['slot'] definition_id = props['definitionId'] - definition = defs[definition_id] + definition = defs.get(definition_id) if not definition: raise RuntimeError( 'No definition under def id {}'.format(definition_id)) @@ -52,7 +52,7 @@ def load_labware(protocol_data): return loaded_labware -def _get_location(loaded_labware, command_type, params, default_values): +def _get_location(loaded_labware, command_type, params): labwareId = params.get('labware') if not labwareId: # not all commands use labware param @@ -64,23 +64,10 @@ def _get_location(loaded_labware, command_type, params, default_values): 'Command tried to use labware "{}", but that ID does not exist ' + 'in protocol\'s "labware" section'.format(labwareId)) - # default offset from bottom for aspirate/dispense commands - offset_default = default_values.get( - '{}MmFromBottom'.format(command_type)) - - # optional command-specific value, fallback to default - offset_from_bottom = params.get( - 'offsetFromBottomMm', offset_default) + offset_from_bottom = params.get('offsetFromBottomMm') if offset_from_bottom is None: - # not all commands use offsets - - # touch-tip uses offset from top, not bottom, as default - # when offsetFromBottomMm command-specific value is unset - if command_type == 'touchTip': - return lw.wells(well).top( - z=default_values['touchTipMmFromTop']) - + # not all commands use offsets (eg pick up tip / drop tip) return lw.wells(well) return lw.wells(well).bottom(offset_from_bottom) @@ -94,41 +81,21 @@ def _get_pipette(command_params, loaded_pipettes): # TODO (Ian 2018-08-22) once Pipette has more sensible way of managing # flow rate value (eg as an argument in aspirate/dispense fns), remove this def _set_flow_rate( - pipette_name, pipette, command_type, params, default_values): + pipette_name, pipette, command_type, params): """ - Set flow rate in uL/mm, to value obtained from command's params, - or if unspecified in command params, then from protocol's "defaultValues". + Set flow rate in uL/mm, to value obtained from command's params """ - default_aspirate = default_values.get( - 'aspirateFlowRate', {}).get(pipette_name) - - default_dispense = default_values.get( - 'dispenseFlowRate', {}).get(pipette_name) - flow_rate_param = params.get('flowRate') - if flow_rate_param is not None: - if command_type == 'aspirate': - pipette.set_flow_rate( - aspirate=flow_rate_param, - dispense=default_dispense) - return - if command_type == 'dispense': - pipette.set_flow_rate( - aspirate=default_aspirate, - dispense=flow_rate_param) - return - pipette.set_flow_rate( - aspirate=default_aspirate, - dispense=default_dispense - ) + aspirate=flow_rate_param, + dispense=flow_rate_param) + return # C901 code complexity is due to long elif block, ok in this case (Ian+Ben) def dispatch_commands(protocol_data, loaded_pipettes, loaded_labware): # noqa: C901 E501 commands = protocol_data['commands'] - default_values = protocol_data.get('defaultValues', {}) for command_item in commands: command_type = command_item['command'] @@ -141,7 +108,7 @@ def dispatch_commands(protocol_data, loaded_pipettes, loaded_labware): # noqa: pipette_name = protocol_pipette_data.get('name') location = _get_location( - loaded_labware, command_type, params, default_values) + loaded_labware, command_type, params) volume = params.get('volume') if pipette: @@ -150,7 +117,7 @@ def dispatch_commands(protocol_data, loaded_pipettes, loaded_labware): # noqa: # Flow rate is persisted inside the Pipette object # and is settable but not easily gettable _set_flow_rate( - pipette_name, pipette, command_type, params, default_values) + pipette_name, pipette, command_type, params) if command_type == 'delay': wait = params.get('wait') @@ -188,8 +155,7 @@ def dispatch_commands(protocol_data, loaded_pipettes, loaded_labware): # noqa: (well_object, loc_tuple) = location # Use the offset baked into the well_object. - # Do not allow API to apply its v_offset kwarg default value, - # and do not apply the JSON protocol's default offset. + # Do not allow API to apply its v_offset kwarg default value z_from_bottom = loc_tuple[2] offset_from_top = ( well_object.properties['depth'] - z_from_bottom) * -1 diff --git a/api/tests/opentrons/protocols/test_execute_v3.py b/api/tests/opentrons/protocols/test_execute_v3.py index 8c9da7f6038..cf6cc60a8d5 100644 --- a/api/tests/opentrons/protocols/test_execute_v3.py +++ b/api/tests/opentrons/protocols/test_execute_v3.py @@ -32,10 +32,6 @@ def test_get_location(): plate = labware.load("96-flat", 1) well = "B2" - default_values = { - 'aspirateMmFromBottom': 2 - } - loaded_labware = { "someLabwareId": plate } @@ -48,20 +44,9 @@ def test_get_location(): "offsetFromBottomMm": offset } result = execute_v3._get_location( - loaded_labware, command_type, command_params, default_values) + loaded_labware, command_type, command_params) assert result == plate.well(well).bottom(offset) - command_params = { - "labware": "someLabwareId", - "well": well - } - - # no command-specific offset, use default - result = execute_v3._get_location( - loaded_labware, command_type, command_params, default_values) - assert result == plate.well(well).bottom( - default_values['aspirateMmFromBottom']) - def test_load_labware(get_labware_fixture): robot.reset() @@ -148,6 +133,9 @@ def mock_set_flow_rate(aspirate, dispense): monkeypatch.setattr(pipette, 'set_flow_rate', mock_set_flow_rate) monkeypatch.setattr(execute_v3, '_sleep', mock_sleep) + aspirateOffset = 12.1 + dispenseOffset = 12.2 + protocol_data = { "defaultValues": { "aspirateFlowRate": { @@ -171,7 +159,8 @@ def mock_set_flow_rate(aspirate, dispense): "labware": "sourcePlateId", "well": "A1", "volume": 5, - "flowRate": 123 + "flowRate": 123, + "offsetFromBottomMm": aspirateOffset } }, { @@ -186,7 +175,9 @@ def mock_set_flow_rate(aspirate, dispense): "pipette": "pipetteId", "labware": "destPlateId", "well": "B1", - "volume": 4.5 + "volume": 4.5, + "flowRate": 3.5, + "offsetFromBottomMm": dispenseOffset } }, ] @@ -196,12 +187,12 @@ def mock_set_flow_rate(aspirate, dispense): protocol_data, loaded_pipettes, loaded_labware) assert cmd == [ - ("aspirate", 5, source_plate['A1']), + ("aspirate", 5, source_plate['A1'].bottom(aspirateOffset)), ("sleep", 42), - ("dispense", 4.5, dest_plate['B1']) + ("dispense", 4.5, dest_plate['B1'].bottom(dispenseOffset)) ] assert flow_rates == [ - (123, 102), - (101, 102) + (123, 123), + (3.5, 3.5) ] diff --git a/app/src/protocol/types.js b/app/src/protocol/types.js index f0a05451b7a..092129ad485 100644 --- a/app/src/protocol/types.js +++ b/app/src/protocol/types.js @@ -1,9 +1,7 @@ // @flow // protocol type defs -import type { - SchemaV1ProtocolFile, - SchemaV3ProtocolFile, -} from '@opentrons/shared-data' +import type { ProtocolFile as SchemaV1ProtocolFile } from '@opentrons/shared-data/protocol/flowTypes/schemaV1' +import type { ProtocolFile as SchemaV3ProtocolFile } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' // data may be a full JSON protocol or just a metadata dict from Python export type ProtocolData = diff --git a/protocol-designer/src/components/FileSidebar/ConnectedFileSidebar.js b/protocol-designer/src/components/FileSidebar/ConnectedFileSidebar.js index acce512d72d..d5c8a10f148 100644 --- a/protocol-designer/src/components/FileSidebar/ConnectedFileSidebar.js +++ b/protocol-designer/src/components/FileSidebar/ConnectedFileSidebar.js @@ -33,7 +33,7 @@ export default connect( function mapStateToProps(state: BaseState): SP { const protocolName = - fileDataSelectors.getFileMetadata(state)['protocol-name'] || 'untitled' + fileDataSelectors.getFileMetadata(state).protocolName || 'untitled' const fileData = fileDataSelectors.createFile(state) const canDownload = selectors.getCurrentPage(state) !== 'file-splash' diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPosition/TipPositionModal.js b/protocol-designer/src/components/StepEditForm/fields/TipPosition/TipPositionModal.js index b5f8de4d79b..17e5daafa04 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPosition/TipPositionModal.js +++ b/protocol-designer/src/components/StepEditForm/fields/TipPosition/TipPositionModal.js @@ -34,7 +34,7 @@ const DECIMALS_ALLOWED = 1 type OP = {| mmFromBottom: number, - wellHeightMM: number, + wellDepthMm: number, isOpen: boolean, closeModal: () => mixed, fieldName: TipOffsetFields, @@ -57,7 +57,7 @@ class TipPositionModal extends React.Component { this.state = { value: initialValue } } componentDidUpdate(prevProps: Props) { - if (prevProps.wellHeightMM !== this.props.wellHeightMM) { + if (prevProps.wellDepthMm !== this.props.wellDepthMm) { this.setState({ value: roundValue(this.props.mmFromBottom) }) } } @@ -67,8 +67,8 @@ class TipPositionModal extends React.Component { this.props.closeModal() } getDefaultMmFromBottom = (): number => { - const { fieldName, wellHeightMM } = this.props - return utils.getDefaultMmFromBottom({ fieldName, wellHeightMM }) + const { fieldName, wellDepthMm } = this.props + return utils.getDefaultMmFromBottom({ fieldName, wellDepthMm }) } getMinMaxMmFromBottom = (): { maxMmFromBottom: number, @@ -76,12 +76,12 @@ class TipPositionModal extends React.Component { } => { if (getIsTouchTipField(this.props.fieldName)) { return { - maxMmFromBottom: roundValue(this.props.wellHeightMM), - minMmFromBottom: roundValue(this.props.wellHeightMM / 2), + maxMmFromBottom: roundValue(this.props.wellDepthMm), + minMmFromBottom: roundValue(this.props.wellDepthMm / 2), } } return { - maxMmFromBottom: roundValue(this.props.wellHeightMM * 2), + maxMmFromBottom: roundValue(this.props.wellDepthMm * 2), minMmFromBottom: 0, } } @@ -139,7 +139,7 @@ class TipPositionModal extends React.Component { render() { if (!this.props.isOpen) return null const { value } = this.state - const { fieldName, wellHeightMM } = this.props + const { fieldName, wellDepthMm } = this.props const { maxMmFromBottom, minMmFromBottom } = this.getMinMaxMmFromBottom() return ( @@ -209,7 +209,7 @@ class TipPositionModal extends React.Component { mmFromBottom={ value != null ? value : this.getDefaultMmFromBottom() } - wellHeightMM={wellHeightMM} + wellDepthMm={wellDepthMm} /> diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPosition/TipPositionZAxisViz.js b/protocol-designer/src/components/StepEditForm/fields/TipPosition/TipPositionZAxisViz.js index caf477c6ab4..25de5bedc13 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPosition/TipPositionZAxisViz.js +++ b/protocol-designer/src/components/StepEditForm/fields/TipPosition/TipPositionZAxisViz.js @@ -11,15 +11,15 @@ const WELL_HEIGHT_PIXELS = 48 const PIXEL_DECIMALS = 2 type Props = { mmFromBottom: number, - wellHeightMM: number, + wellDepthMm: number, } const TipPositionZAxisViz = (props: Props) => { - const fractionOfWellHeight = props.mmFromBottom / props.wellHeightMM + const fractionOfWellHeight = props.mmFromBottom / props.wellDepthMm const pixelsFromBottom = Number(fractionOfWellHeight) * WELL_HEIGHT_PIXELS - WELL_HEIGHT_PIXELS const roundedPixelsFromBottom = round(pixelsFromBottom, PIXEL_DECIMALS) - const bottomPx = props.wellHeightMM + const bottomPx = props.wellDepthMm ? roundedPixelsFromBottom : props.mmFromBottom - WELL_HEIGHT_PIXELS return ( @@ -29,8 +29,8 @@ const TipPositionZAxisViz = (props: Props) => { className={styles.pipette_tip_image} style={{ bottom: `${bottomPx}px` }} /> - {props.wellHeightMM !== null && ( - {props.wellHeightMM}mm + {props.wellDepthMm !== null && ( + {props.wellDepthMm}mm )} { state: TipPositionInputState = { isModalOpen: false } handleOpen = () => { - if (this.props.wellHeightMM) { + if (this.props.wellDepthMm) { this.setState({ isModalOpen: true }) } } @@ -52,7 +53,7 @@ class TipPositionInput extends React.Component { } render() { - const { disabled, fieldName, mmFromBottom, wellHeightMM } = this.props + const { disabled, fieldName, mmFromBottom, wellDepthMm } = this.props const isTouchTipField = getIsTouchTipField(this.props.fieldName) const Wrapper = ({ children, hoverTooltipHandlers }) => @@ -70,12 +71,12 @@ class TipPositionInput extends React.Component { ) let value = '' - if (wellHeightMM != null) { + if (wellDepthMm != null) { // show default value for field in parens if no mmFromBottom value is selected value = mmFromBottom != null ? mmFromBottom - : getDefaultMmFromBottom({ fieldName, wellHeightMM }) + : getDefaultMmFromBottom({ fieldName, wellDepthMm }) } return ( @@ -87,7 +88,7 @@ class TipPositionInput extends React.Component { { ownProps.fieldName ) - let wellHeightMM = null + let wellDepthMm = null const labwareId: ?string = rawForm && rawForm[labwareFieldName] if (labwareId != null) { const labwareDef = stepFormSelectors.getLabwareEntities(state)[labwareId] @@ -121,12 +122,12 @@ const mapSTP = (state: BaseState, ownProps: OP): SP => { // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths const firstWell = labwareDef.wells['A1'] - if (firstWell) wellHeightMM = firstWell.depth + if (firstWell) wellDepthMm = getWellsDepth(labwareDef, ['A1']) } return { disabled: rawForm ? getDisabledFields(rawForm).has(fieldName) : false, - wellHeightMM, + wellDepthMm, mmFromBottom: rawForm && rawForm[fieldName], } } diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPosition/utils.js b/protocol-designer/src/components/StepEditForm/fields/TipPosition/utils.js index 50442758bf5..223a06b6951 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPosition/utils.js +++ b/protocol-designer/src/components/StepEditForm/fields/TipPosition/utils.js @@ -15,9 +15,9 @@ import { // name to infer step type! export function getDefaultMmFromBottom(args: { fieldName: TipOffsetFields, - wellHeightMM: number, + wellDepthMm: number, }): number { - const { fieldName, wellHeightMM } = args + const { fieldName, wellDepthMm } = args switch (fieldName) { case 'aspirate_mmFromBottom': return DEFAULT_MM_FROM_BOTTOM_ASPIRATE @@ -32,6 +32,6 @@ export function getDefaultMmFromBottom(args: { getIsTouchTipField(fieldName), `getDefaultMmFromBottom fn does not know what to do with field ${fieldName}` ) - return DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP + wellHeightMM + return DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP + wellDepthMm } } diff --git a/protocol-designer/src/components/modals/FilePipettesModal/TiprackDiagram.js b/protocol-designer/src/components/modals/FilePipettesModal/TiprackDiagram.js index ad6fd74ae70..498330b89e0 100644 --- a/protocol-designer/src/components/modals/FilePipettesModal/TiprackDiagram.js +++ b/protocol-designer/src/components/modals/FilePipettesModal/TiprackDiagram.js @@ -27,7 +27,7 @@ function TiprackDiagram(props: Props) { const mapStateToProps = (state: BaseState, ownProps: OP): SP => { const { definitionURI } = ownProps const definition = definitionURI - ? labwareDefSelectors.getLabwareDefsById(state)[definitionURI] + ? labwareDefSelectors.getLabwareDefsByURI(state)[definitionURI] : null return { definition } } diff --git a/protocol-designer/src/components/steplist/PauseStepItems.js b/protocol-designer/src/components/steplist/PauseStepItems.js index 9b6a96dfac3..542617f40a2 100644 --- a/protocol-designer/src/components/steplist/PauseStepItems.js +++ b/protocol-designer/src/components/steplist/PauseStepItems.js @@ -1,10 +1,10 @@ // @flow import * as React from 'react' -import type { DelayArgs } from '../../step-generation' +import type { PauseArgs } from '../../step-generation' import { PDListItem } from '../lists' type Props = { - pauseArgs: DelayArgs, + pauseArgs: PauseArgs, } export default function PauseStepItems(props: Props) { diff --git a/protocol-designer/src/constants.js b/protocol-designer/src/constants.js index e430f2bc995..0b177a7c613 100644 --- a/protocol-designer/src/constants.js +++ b/protocol-designer/src/constants.js @@ -33,11 +33,13 @@ export const END_TERMINAL_TITLE = 'FINAL DECK STATE' export const INITIAL_DECK_SETUP_STEP_ID = '__INITIAL_DECK_SETUP_STEP__' export const DEFAULT_CHANGE_TIP_OPTION: 'always' = 'always' + +// TODO: Ian 2019-06-13 don't keep these as hard-coded static values (see #3587) export const DEFAULT_MM_FROM_BOTTOM_ASPIRATE = 1 export const DEFAULT_MM_FROM_BOTTOM_DISPENSE = 0.5 - // NOTE: in the negative Z direction, to go down from top export const DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP = -1 +export const DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP = 0 export const DEFAULT_WELL_ORDER_FIRST_OPTION: 't2b' = 't2b' export const DEFAULT_WELL_ORDER_SECOND_OPTION: 'l2r' = 'l2r' diff --git a/protocol-designer/src/file-data/reducers/index.js b/protocol-designer/src/file-data/reducers/index.js index 5745d0aded2..38ce0cb2654 100644 --- a/protocol-designer/src/file-data/reducers/index.js +++ b/protocol-designer/src/file-data/reducers/index.js @@ -8,7 +8,7 @@ import type { FileMetadataFields } from '../types' import type { LoadFileAction, NewProtocolFields } from '../../load-file' const defaultFields = { - 'protocol-name': '', + protocolName: '', author: '', description: '', } @@ -36,7 +36,7 @@ function newProtocolMetadata( ): FileMetadataFields { return { ...defaultFields, - 'protocol-name': action.payload.name || '', + protocolName: action.payload.name || '', created: Date.now(), } } @@ -54,7 +54,7 @@ const fileMetadata = handleActions( }), SAVE_PROTOCOL_FILE: (state: FileMetadataFields): FileMetadataFields => { // NOTE: 'last-modified' is updated "on-demand", in response to user clicking "save/export" - return { ...state, 'last-modified': Date.now() } + return { ...state, lastModified: Date.now() } }, }, defaultFields diff --git a/protocol-designer/src/file-data/selectors/fileCreator.js b/protocol-designer/src/file-data/selectors/fileCreator.js index d1db7bdcadc..205701e038c 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.js +++ b/protocol-designer/src/file-data/selectors/fileCreator.js @@ -1,11 +1,13 @@ // @flow import { createSelector } from 'reselect' -import mapValues from 'lodash/mapValues' +import flatten from 'lodash/flatten' import isEmpty from 'lodash/isEmpty' -import { getFlowRateDefaultsAllPipettes } from '@opentrons/shared-data' +import mapValues from 'lodash/mapValues' +import uniq from 'lodash/uniq' import { getFileMetadata } from './fileFields' import { getInitialRobotState, getRobotStateTimeline } from './commands' import { selectors as dismissSelectors } from '../../dismiss' +import { selectors as labwareDefSelectors } from '../../labware-defs' import { selectors as ingredSelectors } from '../../labware-ingred/selectors' import { selectors as stepFormSelectors } from '../../step-forms' import { selectors as uiLabwareSelectors } from '../../ui/labware' @@ -13,40 +15,30 @@ import { DEFAULT_MM_FROM_BOTTOM_ASPIRATE, DEFAULT_MM_FROM_BOTTOM_DISPENSE, DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, + DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } from '../../constants' + import type { - FilePipetteV3 as FilePipette, - FileLabwareV3 as FileLabware, -} from '@opentrons/shared-data' + FilePipette, + FileLabware, + Command, +} from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { BaseState } from '../../types' import type { PDProtocolFile } from '../../file-types' -// TODO LATER Ian 2018-02-28 deal with versioning -const protocolSchemaVersion = '1.0.0' +const protocolSchemaVersion = 3 // TODO: BC: 2018-02-21 uncomment this assert, causes test failures // assert(!isEmpty(process.env.OT_PD_VERSION), 'Could not find application version!') if (isEmpty(process.env.OT_PD_VERSION)) console.warn('Could not find application version!') -const applicationVersion = process.env.OT_PD_VERSION +const applicationVersion: string = process.env.OT_PD_VERSION || '' // Internal release date: this should never be read programatically, // it just helps us humans quickly identify what build a user was using // when we look at saved protocols (without requiring us to trace thru git logs) const _internalAppBuildDate = process.env.OT_PD_BUILD_DATE -const executionDefaults: $PropertyType = { - 'aspirate-flow-rate': getFlowRateDefaultsAllPipettes( - 'defaultAspirateFlowRate' - ), - 'dispense-flow-rate': getFlowRateDefaultsAllPipettes( - 'defaultDispenseFlowRate' - ), - 'aspirate-mm-from-bottom': DEFAULT_MM_FROM_BOTTOM_ASPIRATE, - 'dispense-mm-from-bottom': DEFAULT_MM_FROM_BOTTOM_DISPENSE, - 'touch-tip-mm-from-top': DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, -} - export const createFile: BaseState => PDProtocolFile = createSelector( getFileMetadata, getInitialRobotState, @@ -59,6 +51,7 @@ export const createFile: BaseState => PDProtocolFile = createSelector( stepFormSelectors.getLabwareEntities, stepFormSelectors.getPipetteEntities, uiLabwareSelectors.getLabwareNicknamesById, + labwareDefSelectors.getLabwareDefsByURI, ( fileMetadata, initialRobotState, @@ -70,11 +63,12 @@ export const createFile: BaseState => PDProtocolFile = createSelector( orderedStepIds, labwareEntities, pipetteEntities, - labwareNamesById + labwareNicknamesById, + labwareDefsByURI ) => { const { author, description, created } = fileMetadata - const name = fileMetadata['protocol-name'] || 'untitled' - const lastModified = fileMetadata['last-modified'] + const name = fileMetadata.protocolName || 'untitled' + const lastModified = fileMetadata.lastModified const pipettes = mapValues( initialRobotState.pipettes, @@ -83,23 +77,19 @@ export const createFile: BaseState => PDProtocolFile = createSelector( pipetteId: string ): FilePipette => ({ mount: pipette.mount, - // TODO: Ian 2018-11-06 'model' is for backwards compatibility with old API version - // (JSON executor used to expect versioned model). - // Drop this "model" when we do breaking change (see TODO in protocol-schema.json) - model: pipetteEntities[pipetteId].name + '_v1.3', name: pipetteEntities[pipetteId].name, }) ) - const labware = mapValues( + const labware: { [labwareId: string]: FileLabware } = mapValues( initialRobotState.labware, ( l: $Values, labwareId: string ): FileLabware => ({ slot: l.slot, - 'display-name': labwareNamesById[labwareId], - labwareDefURI: labwareEntities[labwareId].labwareDefURI, + displayName: labwareNicknamesById[labwareId], + definitionId: labwareEntities[labwareId].labwareDefURI, }) ) @@ -110,15 +100,29 @@ export const createFile: BaseState => PDProtocolFile = createSelector( stepId => savedStepForms[stepId] ) + // exclude definitions that aren't used by any labware in the protocol + const labwareDefsInUse = uniq( + Object.keys(labware).map( + (labwareId: string) => labware[labwareId].definitionId + ) + ) + const labwareDefinitions = labwareDefsInUse.reduce( + (acc, labwareDefURI: string) => ({ + ...acc, + [labwareDefURI]: labwareDefsByURI[labwareDefURI], + }), + {} + ) + return { - 'protocol-schema': protocolSchemaVersion, + schemaVersion: protocolSchemaVersion, metadata: { - 'protocol-name': name, + protocolName: name, author, description, created, - 'last-modified': lastModified, + lastModified, // TODO LATER category: null, @@ -126,14 +130,20 @@ export const createFile: BaseState => PDProtocolFile = createSelector( tags: [], }, - 'default-values': executionDefaults, - - 'designer-application': { - 'application-name': 'opentrons/protocol-designer', - 'application-version': applicationVersion, - applicationVersion, - _internalAppBuildDate, + designerApplication: { + name: 'opentrons/protocol-designer', + version: applicationVersion, data: { + _internalAppBuildDate, + defaultValues: { + // TODO: Ian 2019-06-13 load these into redux and always get them from redux, not constants.js + // This `defaultValues` key is not yet read by anything, but is populated here for auditability + // and so that later we can do #3587 without a PD migration + aspirate_mmFromBottom: DEFAULT_MM_FROM_BOTTOM_ASPIRATE, + dispense_mmFromBottom: DEFAULT_MM_FROM_BOTTOM_DISPENSE, + touchTip_mmFromTop: DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, + blowout_mmFromTop: DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, + }, pipetteTiprackAssignments: mapValues( pipetteEntities, (p: $Values): ?string => p.tiprackDefURI @@ -152,14 +162,11 @@ export const createFile: BaseState => PDProtocolFile = createSelector( pipettes, labware, + labwareDefinitions, - procedure: robotStateTimeline.timeline.map((timelineItem, i) => ({ - annotation: { - name: `TODO Name ${i}`, - description: 'todo description', - }, - subprocedure: timelineItem.commands.reduce((acc, c) => [...acc, c], []), - })), + commands: flatten( + robotStateTimeline.timeline.map(timelineFrame => timelineFrame.commands) + ), } } ) diff --git a/protocol-designer/src/file-data/selectors/fileFields.js b/protocol-designer/src/file-data/selectors/fileFields.js index 255ee7524a3..0810c484b07 100644 --- a/protocol-designer/src/file-data/selectors/fileFields.js +++ b/protocol-designer/src/file-data/selectors/fileFields.js @@ -12,10 +12,10 @@ export const getCurrentProtocolExists: Selector = createSelector( ) export const protocolName: Selector< - $PropertyType + $PropertyType > = createSelector( rootSelector, - state => state.fileMetadata['protocol-name'] + state => state.fileMetadata.protocolName ) export const getFileMetadata: Selector = createSelector( diff --git a/protocol-designer/src/file-data/types.js b/protocol-designer/src/file-data/types.js index 9812290708f..ace9c133910 100644 --- a/protocol-designer/src/file-data/types.js +++ b/protocol-designer/src/file-data/types.js @@ -1,8 +1,5 @@ // @flow -import type { SchemaV1ProtocolFile } from '@opentrons/shared-data' -export type FileMetadataFields = $PropertyType< - SchemaV1ProtocolFile<{}>, - 'metadata' -> +import type { ProtocolFile } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' +export type FileMetadataFields = $PropertyType, 'metadata'> export type FileMetadataFieldAccessors = $Keys diff --git a/protocol-designer/src/file-types.js b/protocol-designer/src/file-types.js index fee8ab0c19c..4df81ce3b29 100644 --- a/protocol-designer/src/file-types.js +++ b/protocol-designer/src/file-types.js @@ -2,7 +2,7 @@ import type { RootState as IngredRoot } from './labware-ingred/reducers' import type { RootState as StepformRoot } from './step-forms' import type { RootState as DismissRoot } from './dismiss' -import type { SchemaV1ProtocolFile } from '@opentrons/shared-data' +import type { ProtocolFile } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' export type PDMetadata = { // pipetteId to tiprackModel @@ -15,10 +15,21 @@ export type PDMetadata = { savedStepForms: $PropertyType, orderedStepIds: $PropertyType, + + defaultValues: { + aspirate_mmFromBottom: number, + dispense_mmFromBottom: number, + touchTip_mmFromTop: number, + blowout_mmFromTop: number, + }, } -export type PDProtocolFile = SchemaV1ProtocolFile +export type PDProtocolFile = ProtocolFile export function getPDMetadata(file: PDProtocolFile): PDMetadata { - return file['designer-application'].data + const metadata = file.designerApplication?.data + if (!metadata) { + throw new Error('expected designerApplication.data in file') + } + return metadata } diff --git a/protocol-designer/src/form-types.js b/protocol-designer/src/form-types.js index 67e8bda7fb8..ee181d1e44c 100644 --- a/protocol-designer/src/form-types.js +++ b/protocol-designer/src/form-types.js @@ -105,7 +105,7 @@ export type MixForm = {| times?: string, volume?: string, wells?: Array, - 'touch-tip'?: boolean, + touchTip?: boolean, |} export type PauseForm = {| @@ -185,6 +185,32 @@ export type HydratedMoveLiquidFormData = { }, } +export type HydratedMixFormDataLegacy = { + id: string, + stepType: 'mix', + stepName: string, + stepDetails: ?string, + + pipette: PipetteEntity, + volume: number, + changeTip: ChangeTipOptions, + + labware: LabwareEntity, + wells: Array, + mix_wellOrder_first: WellOrderOption, + mix_wellOrder_second: WellOrderOption, + aspirate_flowRate: ?number, + mix_mmFromBottom: ?number, + mix_touchTip_checkbox: ?boolean, + mix_touchTip_mmFromBottom: ?number, + times: ?number, + + dispense_flowRate: ?number, + + blowout_checkbox: ?boolean, + blowout_location: ?string, // labwareId or 'SOURCE_WELL' or 'DEST_WELL' +} + // TODO: Ian 2019-01-17 Moving away from this and towards nesting all form fields // inside `fields` key, but deprecating transfer/consolidate/distribute is a pre-req export type HydratedMoveLiquidFormDataLegacy = { diff --git a/protocol-designer/src/labware-defs/selectors.js b/protocol-designer/src/labware-defs/selectors.js index 9b8c2ecf47a..46d592422b7 100644 --- a/protocol-designer/src/labware-defs/selectors.js +++ b/protocol-designer/src/labware-defs/selectors.js @@ -83,7 +83,7 @@ export const _getLabwareDefsByIdRootState: StepFormRootState => LabwareDefByDefU _makeLabwareDefsObj ) -export const getLabwareDefsById: Selector = createSelector( +export const getLabwareDefsByURI: Selector = createSelector( state => rootSelector(state).customDefs, _makeLabwareDefsObj ) diff --git a/protocol-designer/src/labware-ingred/reducers/index.js b/protocol-designer/src/labware-ingred/reducers/index.js index 0c3d4ec443d..a7cc6b0e05f 100644 --- a/protocol-designer/src/labware-ingred/reducers/index.js +++ b/protocol-designer/src/labware-ingred/reducers/index.js @@ -176,10 +176,11 @@ export const containers = handleActions( return sortedLabwareIds.reduce( (acc: ContainersState, id): ContainersState => { const fileLabware = allFileLabware[id] - const nickname = fileLabware['display-name'] + const nickname = fileLabware.displayName const disambiguationNumber = Object.keys(acc).filter( - filterId => allFileLabware[filterId]['display-name'] === nickname + (filterId: string) => + allFileLabware[filterId].displayName === nickname ).length + 1 return { ...acc, diff --git a/protocol-designer/src/labware-ingred/types.js b/protocol-designer/src/labware-ingred/types.js index 28965779b6c..d3c6b86ebe6 100644 --- a/protocol-designer/src/labware-ingred/types.js +++ b/protocol-designer/src/labware-ingred/types.js @@ -13,7 +13,7 @@ export type LabwareTypeById = { [labwareId: string]: ?string } // ==== WELLS ========== -// TODO: Ian 2019-07-08 remove this in favor of WellGroup +// TODO: Ian 2019-06-08 remove this in favor of WellGroup export type Wells = { [wellName: string]: string, // eg A1: 'A1'. } diff --git a/protocol-designer/src/load-file/migration/1_1_0.js b/protocol-designer/src/load-file/migration/1_1_0.js index d1be80dc653..faefaf45393 100644 --- a/protocol-designer/src/load-file/migration/1_1_0.js +++ b/protocol-designer/src/load-file/migration/1_1_0.js @@ -7,13 +7,46 @@ import omitBy from 'lodash/omitBy' import flow from 'lodash/flow' import { getLabware, getPipetteNameSpecs } from '@opentrons/shared-data' import type { - FileLabwareV1 as FileLabware, - FilePipetteV1 as FilePipette, -} from '@opentrons/shared-data' + FileLabware, + FilePipette, + ProtocolFile, +} from '@opentrons/shared-data/protocol/flowTypes/schemaV1' import type { FormPatch } from '../../steplist/actions' -import type { PDProtocolFile } from '../../file-types' import type { FormData } from '../../form-types' +type PDV1Metadata = { + pipetteTiprackAssignments: { [pipetteId: string]: string }, + + dismissedWarnings: { + form: { [stepId: string]: ?Array }, + timeline: { [stepId: string]: ?Array }, + }, + + ingredients: { + [groupId: string]: { + name: ?string, + description: ?string, + serialize: boolean, + }, + }, + ingredLocations: { + [labwareId: string]: { + [well: string]: { [ingredGroup: string]: { volume: number } }, + }, + }, + + savedStepForms: { + [stepId: string]: { + stepType: 'moveLiquid' | 'mix' | 'pause' | 'manualIntervention', + id: string, + [string]: any, + }, + }, + orderedStepIds: Array, +} + +type PDProtocolFile = ProtocolFile + type LegacyPipetteEntities = { [pipetteId: string]: { id: string, diff --git a/protocol-designer/src/load-file/migration/index.js b/protocol-designer/src/load-file/migration/index.js index 8cae342ac80..0b1c1c9a053 100644 --- a/protocol-designer/src/load-file/migration/index.js +++ b/protocol-designer/src/load-file/migration/index.js @@ -8,8 +8,7 @@ import migrateTo_1_1_0 from './1_1_0' export const OLDEST_MIGRATEABLE_VERSION = '1.0.0' type Version = string -type Migration = PDProtocolFile => PDProtocolFile -type MigrationsByVersion = { [Version]: Migration } +type MigrationsByVersion = { [Version]: (Object) => Object } const allMigrationsByVersion: MigrationsByVersion = { '1.1.0': migrateTo_1_1_0, diff --git a/protocol-designer/src/step-forms/reducers/index.js b/protocol-designer/src/step-forms/reducers/index.js index dc4fcadc913..06983645ab2 100644 --- a/protocol-designer/src/step-forms/reducers/index.js +++ b/protocol-designer/src/step-forms/reducers/index.js @@ -33,10 +33,11 @@ import type { SwapSlotContentsAction, } from '../../labware-ingred/actions' import type { FormData, StepIdType } from '../../form-types' +// TODO: Ian 2019-06-12 update labware & pipette state shape to not use v1 #3336 import type { - FileLabwareV1 as FileLabware, - FilePipetteV1 as FilePipette, -} from '@opentrons/shared-data' + FileLabware, + FilePipette, +} from '@opentrons/shared-data/protocol/flowTypes/schemaV1' import type { AddStepAction, diff --git a/protocol-designer/src/step-forms/selectors/index.js b/protocol-designer/src/step-forms/selectors/index.js index 98e8390f433..bbcbc366b81 100644 --- a/protocol-designer/src/step-forms/selectors/index.js +++ b/protocol-designer/src/step-forms/selectors/index.js @@ -80,7 +80,7 @@ function _hydrateLabwareEntity( export const getLabwareEntities: Selector = createSelector( _getNormalizedLabwareById, - labwareDefSelectors.getLabwareDefsById, + labwareDefSelectors.getLabwareDefsByURI, (normalizedLabwareById, labwareDefs) => mapValues(normalizedLabwareById, (l: NormalizedLabware, id: string) => _hydrateLabwareEntity(l, id, labwareDefs) @@ -99,7 +99,7 @@ export const _getLabwareEntitiesRootState: RootState => LabwareEntities = create export const getPipetteEntities: Selector = createSelector( state => rootSelector(state).pipetteInvariantProperties, - labwareDefSelectors.getLabwareDefsById, + labwareDefSelectors.getLabwareDefsByURI, denormalizePipetteEntities ) diff --git a/protocol-designer/src/step-generation/commandCreators/atomic/aspirate.js b/protocol-designer/src/step-generation/commandCreators/atomic/aspirate.js index c6e238b85a4..7175a1e7b0d 100644 --- a/protocol-designer/src/step-generation/commandCreators/atomic/aspirate.js +++ b/protocol-designer/src/step-generation/commandCreators/atomic/aspirate.js @@ -2,7 +2,7 @@ import getNextRobotStateAndWarnings from '../../getNextRobotStateAndWarnings' import * as errorCreators from '../../errorCreators' import { getPipetteWithTipMaxVol } from '../../robotStateSelectors' -import type { AspirateDispenseArgsV1 as AspirateDispenseArgs } from '@opentrons/shared-data' +import type { AspirateParams as AspirateDispenseArgs } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { InvariantContext, RobotState, @@ -15,8 +15,7 @@ const aspirate = (args: AspirateDispenseArgs): CommandCreator => ( invariantContext: InvariantContext, prevRobotState: RobotState ) => { - const { pipette, volume, labware, well, offsetFromBottomMm } = args - const flowRateUlSec = args['flow-rate'] + const { pipette, volume, labware, well, offsetFromBottomMm, flowRate } = args const actionName = 'aspirate' let errors: Array = [] @@ -78,9 +77,8 @@ const aspirate = (args: AspirateDispenseArgs): CommandCreator => ( volume, labware, well, - offsetFromBottomMm: - offsetFromBottomMm == null ? undefined : offsetFromBottomMm, - 'flow-rate': flowRateUlSec == null ? undefined : flowRateUlSec, + offsetFromBottomMm, + flowRate, }, }, ] diff --git a/protocol-designer/src/step-generation/commandCreators/atomic/blowout.js b/protocol-designer/src/step-generation/commandCreators/atomic/blowout.js index 9d22e0e4a60..c12302d3c97 100644 --- a/protocol-designer/src/step-generation/commandCreators/atomic/blowout.js +++ b/protocol-designer/src/step-generation/commandCreators/atomic/blowout.js @@ -1,6 +1,6 @@ // @flow import * as errorCreators from '../../errorCreators' -import type { PipetteLabwareFieldsV1 as PipetteLabwareFields } from '@opentrons/shared-data' +import type { BlowoutParams } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { InvariantContext, RobotState, @@ -10,12 +10,12 @@ import type { import updateLiquidState from '../../dispenseUpdateLiquidState' -const blowout = (args: PipetteLabwareFields): CommandCreator => ( +const blowout = (args: BlowoutParams): CommandCreator => ( invariantContext: InvariantContext, prevRobotState: RobotState ) => { /** Blowout with given args. Requires tip. */ - const { pipette, labware, well } = args + const { pipette, labware, well, offsetFromBottomMm, flowRate } = args const actionName = 'blowout' let errors: Array = [] @@ -50,6 +50,8 @@ const blowout = (args: PipetteLabwareFields): CommandCreator => ( pipette, labware, well, + flowRate, + offsetFromBottomMm, }, }, ] diff --git a/protocol-designer/src/step-generation/commandCreators/atomic/delay.js b/protocol-designer/src/step-generation/commandCreators/atomic/delay.js index eb3a535fcb7..d664e02dfdd 100644 --- a/protocol-designer/src/step-generation/commandCreators/atomic/delay.js +++ b/protocol-designer/src/step-generation/commandCreators/atomic/delay.js @@ -1,12 +1,8 @@ // @flow -import type { - DelayArgs, - InvariantContext, - RobotState, - CommandCreator, -} from '../../types' +import type { DelayParams } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' +import type { InvariantContext, RobotState, CommandCreator } from '../../types' -const delay = (args: DelayArgs): CommandCreator => ( +const delay = (args: DelayParams): CommandCreator => ( invariantContext: InvariantContext, prevRobotState: RobotState ) => { diff --git a/protocol-designer/src/step-generation/commandCreators/atomic/dispense.js b/protocol-designer/src/step-generation/commandCreators/atomic/dispense.js index 2d26f2fd69a..f6313cf6eed 100644 --- a/protocol-designer/src/step-generation/commandCreators/atomic/dispense.js +++ b/protocol-designer/src/step-generation/commandCreators/atomic/dispense.js @@ -1,7 +1,7 @@ // @flow import * as errorCreators from '../../errorCreators' import updateLiquidState from '../../dispenseUpdateLiquidState' -import type { AspirateDispenseArgsV1 as AspirateDispenseArgs } from '@opentrons/shared-data' +import type { DispenseParams } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { InvariantContext, RobotState, @@ -10,12 +10,11 @@ import type { } from '../../types' /** Dispense with given args. Requires tip. */ -const dispense = (args: AspirateDispenseArgs): CommandCreator => ( +const dispense = (args: DispenseParams): CommandCreator => ( invariantContext: InvariantContext, prevRobotState: RobotState ) => { - const { pipette, volume, labware, well, offsetFromBottomMm } = args - const flowRateUlSec = args['flow-rate'] + const { pipette, volume, labware, well, offsetFromBottomMm, flowRate } = args const actionName = 'dispense' let errors: Array = [] @@ -42,9 +41,8 @@ const dispense = (args: AspirateDispenseArgs): CommandCreator => ( volume, labware, well, - offsetFromBottomMm: - offsetFromBottomMm == null ? undefined : offsetFromBottomMm, - 'flow-rate': flowRateUlSec == null ? undefined : flowRateUlSec, + offsetFromBottomMm, + flowRate, }, }, ] diff --git a/protocol-designer/src/step-generation/commandCreators/atomic/dropAllTips.js b/protocol-designer/src/step-generation/commandCreators/atomic/dropAllTips.js index a5844889d27..07c5ec78bdd 100644 --- a/protocol-designer/src/step-generation/commandCreators/atomic/dropAllTips.js +++ b/protocol-designer/src/step-generation/commandCreators/atomic/dropAllTips.js @@ -10,7 +10,7 @@ const dropAllTips = (): CommandCreator => ( invariantContext: InvariantContext, prevRobotState: RobotState ) => { - const pipetteIds = Object.keys(prevRobotState.pipettes) + const pipetteIds: Array = Object.keys(prevRobotState.pipettes) const commandCreators = pipetteIds.map(pipetteId => dropTip(pipetteId)) return reduceCommandCreators(commandCreators)( invariantContext, diff --git a/protocol-designer/src/step-generation/commandCreators/atomic/dropTip.js b/protocol-designer/src/step-generation/commandCreators/atomic/dropTip.js index 938f55a8733..9f26a035984 100644 --- a/protocol-designer/src/step-generation/commandCreators/atomic/dropTip.js +++ b/protocol-designer/src/step-generation/commandCreators/atomic/dropTip.js @@ -22,7 +22,7 @@ const dropTip = (pipetteId: string): CommandCreator => ( const commands = [ { - command: 'drop-tip', + command: 'dropTip', params: { pipette: pipetteId, labware: FIXED_TRASH_ID, diff --git a/protocol-designer/src/step-generation/commandCreators/atomic/replaceTip.js b/protocol-designer/src/step-generation/commandCreators/atomic/replaceTip.js index 2a601fd5ed4..1709deb71fe 100644 --- a/protocol-designer/src/step-generation/commandCreators/atomic/replaceTip.js +++ b/protocol-designer/src/step-generation/commandCreators/atomic/replaceTip.js @@ -40,7 +40,7 @@ const replaceTip = (pipetteId: string): CommandCreator => ( ...dropTipResult.commands, // pick up tip command { - command: 'pick-up-tip', + command: 'pickUpTip', params: { pipette: pipetteId, labware: nextTiprack.tiprackId, diff --git a/protocol-designer/src/step-generation/commandCreators/atomic/touchTip.js b/protocol-designer/src/step-generation/commandCreators/atomic/touchTip.js index 5ad7666f191..acd17ef471f 100644 --- a/protocol-designer/src/step-generation/commandCreators/atomic/touchTip.js +++ b/protocol-designer/src/step-generation/commandCreators/atomic/touchTip.js @@ -6,10 +6,10 @@ import type { RobotState, CommandCreator, CommandCreatorError, - TouchTipArgs, } from '../../types' +import type { TouchTipParams } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' -const touchTip = (args: TouchTipArgs): CommandCreator => ( +const touchTip = (args: TouchTipParams): CommandCreator => ( invariantContext: InvariantContext, prevRobotState: RobotState ) => { @@ -35,13 +35,12 @@ const touchTip = (args: TouchTipArgs): CommandCreator => ( const commands = [ { - command: 'touch-tip', + command: 'touchTip', params: { pipette, labware, well, - offsetFromBottomMm: - offsetFromBottomMm == null ? undefined : offsetFromBottomMm, + offsetFromBottomMm, }, }, ] diff --git a/protocol-designer/src/step-generation/commandCreators/compound/consolidate.js b/protocol-designer/src/step-generation/commandCreators/compound/consolidate.js index 63ca5d40691..db8dcdf4443 100644 --- a/protocol-designer/src/step-generation/commandCreators/compound/consolidate.js +++ b/protocol-designer/src/step-generation/commandCreators/compound/consolidate.js @@ -54,8 +54,10 @@ const consolidate = (args: ConsolidateArgs): CompoundCommandCreator => ( const { aspirateFlowRateUlSec, dispenseFlowRateUlSec, + blowoutFlowRateUlSec, aspirateOffsetFromBottomMm, dispenseOffsetFromBottomMm, + blowoutOffsetFromTopMm, } = args const maxWellsPerChunk = Math.floor( @@ -90,7 +92,7 @@ const consolidate = (args: ConsolidateArgs): CompoundCommandCreator => ( volume: args.volume, labware: args.sourceLabware, well: sourceWell, - 'flow-rate': aspirateFlowRateUlSec, + flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, }), ...touchTipAfterAspirateCommand, @@ -156,17 +158,22 @@ const consolidate = (args: ConsolidateArgs): CompoundCommandCreator => ( times: args.mixInDestination.times, aspirateOffsetFromBottomMm, dispenseOffsetFromBottomMm, + aspirateFlowRateUlSec, + dispenseFlowRateUlSec, }) : [] - const blowoutCommand = blowoutUtil( - args.pipette, - args.sourceLabware, - sourceWellChunk[0], - args.destLabware, - args.destWell, - args.blowoutLocation - ) + const blowoutCommand = blowoutUtil({ + pipette: args.pipette, + sourceLabwareId: args.sourceLabware, + sourceWell: sourceWellChunk[0], + destLabwareId: args.destLabware, + destWell: args.destWell, + blowoutLocation: args.blowoutLocation, + flowRate: blowoutFlowRateUlSec, + offsetFromTopMm: blowoutOffsetFromTopMm, + invariantContext, + }) return [ ...tipCommands, @@ -178,11 +185,11 @@ const consolidate = (args: ConsolidateArgs): CompoundCommandCreator => ( volume: args.volume * sourceWellChunk.length, labware: args.destLabware, well: args.destWell, - 'flow-rate': dispenseFlowRateUlSec, + flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, }), - ...touchTipAfterDispenseCommands, ...mixAfterCommands, + ...touchTipAfterDispenseCommands, ...blowoutCommand, ] } diff --git a/protocol-designer/src/step-generation/commandCreators/compound/distribute.js b/protocol-designer/src/step-generation/commandCreators/compound/distribute.js index ee392a63b45..925f9d7b377 100644 --- a/protocol-designer/src/step-generation/commandCreators/compound/distribute.js +++ b/protocol-designer/src/step-generation/commandCreators/compound/distribute.js @@ -1,6 +1,7 @@ // @flow import chunk from 'lodash/chunk' import flatMap from 'lodash/flatMap' +import { getWellsDepth } from '@opentrons/shared-data' import * as errorCreators from '../../errorCreators' import { getPipetteWithTipMaxVol } from '../../robotStateSelectors' import type { @@ -115,7 +116,7 @@ const distribute = (args: DistributeArgs): CompoundCommandCreator => ( volume: args.volume, labware: args.destLabware, well: destWell, - 'flow-rate': dispenseFlowRateUlSec, + flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, }), ...touchTipAfterDispenseCommand, @@ -141,6 +142,14 @@ const distribute = (args: DistributeArgs): CompoundCommandCreator => ( pipette: args.pipette, labware: args.disposalLabware, well: args.disposalWell, + flowRate: args.blowoutFlowRateUlSec, + offsetFromBottomMm: + // NOTE: when we use blowoutLocation as mentioned above, + // we can delegate this top -> bottom transform to blowoutUtil + getWellsDepth( + invariantContext.labwareEntities[args.disposalLabware].def, + [args.disposalWell] + ) + args.blowoutOffsetFromTopMm, }), ] } @@ -178,7 +187,7 @@ const distribute = (args: DistributeArgs): CompoundCommandCreator => ( volume: args.volume * destWellChunk.length + disposalVolume, labware: args.sourceLabware, well: args.sourceWell, - 'flow-rate': aspirateFlowRateUlSec, + flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, }), ...touchTipAfterAspirateCommand, diff --git a/protocol-designer/src/step-generation/commandCreators/compound/mix.js b/protocol-designer/src/step-generation/commandCreators/compound/mix.js index 7ca33a0b89b..76833e33816 100644 --- a/protocol-designer/src/step-generation/commandCreators/compound/mix.js +++ b/protocol-designer/src/step-generation/commandCreators/compound/mix.js @@ -18,10 +18,10 @@ export function mixUtil(args: { well: string, volume: number, times: number, - aspirateOffsetFromBottomMm?: ?number, - dispenseOffsetFromBottomMm?: ?number, - aspirateFlowRateUlSec?: ?number, - dispenseFlowRateUlSec?: ?number, + aspirateOffsetFromBottomMm: number, + dispenseOffsetFromBottomMm: number, + aspirateFlowRateUlSec: number, + dispenseFlowRateUlSec: number, }): Array { const { pipette, @@ -42,7 +42,7 @@ export function mixUtil(args: { labware, well, offsetFromBottomMm: aspirateOffsetFromBottomMm, - 'flow-rate': aspirateFlowRateUlSec, + flowRate: aspirateFlowRateUlSec, }), dispense({ pipette, @@ -50,7 +50,7 @@ export function mixUtil(args: { labware, well, offsetFromBottomMm: dispenseOffsetFromBottomMm, - 'flow-rate': dispenseFlowRateUlSec, + flowRate: dispenseFlowRateUlSec, }), ], times @@ -84,6 +84,8 @@ const mix = (data: MixArgs): CompoundCommandCreator => ( dispenseOffsetFromBottomMm, aspirateFlowRateUlSec, dispenseFlowRateUlSec, + blowoutFlowRateUlSec, + blowoutOffsetFromTopMm, } = data // Errors @@ -128,14 +130,17 @@ const mix = (data: MixArgs): CompoundCommandCreator => ( ] : [] - const blowoutCommand = blowoutUtil( - data.pipette, - data.labware, - well, - data.labware, - well, - data.blowoutLocation - ) + const blowoutCommand = blowoutUtil({ + pipette: data.pipette, + sourceLabwareId: data.labware, + sourceWell: well, + destLabwareId: data.labware, + destWell: well, + blowoutLocation: data.blowoutLocation, + flowRate: blowoutFlowRateUlSec, + offsetFromTopMm: blowoutOffsetFromTopMm, + invariantContext, + }) const mixCommands = mixUtil({ pipette, diff --git a/protocol-designer/src/step-generation/commandCreators/compound/transfer.js b/protocol-designer/src/step-generation/commandCreators/compound/transfer.js index 9b03c154fd4..3f8fbd01661 100644 --- a/protocol-designer/src/step-generation/commandCreators/compound/transfer.js +++ b/protocol-designer/src/step-generation/commandCreators/compound/transfer.js @@ -71,6 +71,8 @@ const transfer = (args: TransferArgs): CompoundCommandCreator => ( dispenseFlowRateUlSec, aspirateOffsetFromBottomMm, dispenseOffsetFromBottomMm, + blowoutFlowRateUlSec, + blowoutOffsetFromTopMm, } = args const effectiveTransferVol = getPipetteWithTipMaxVol( @@ -198,14 +200,17 @@ const transfer = (args: TransferArgs): CompoundCommandCreator => ( }) : [] - const blowoutCommand = blowoutUtil( - args.pipette, - args.sourceLabware, - sourceWell, - args.destLabware, - destWell, - args.blowoutLocation - ) + const blowoutCommand = blowoutUtil({ + pipette: args.pipette, + sourceLabwareId: args.sourceLabware, + sourceWell: sourceWell, + destLabwareId: args.destLabware, + destWell: destWell, + blowoutLocation: args.blowoutLocation, + flowRate: blowoutFlowRateUlSec, + offsetFromTopMm: blowoutOffsetFromTopMm, + invariantContext, + }) const nextCommands = [ ...tipCommands, @@ -216,7 +221,7 @@ const transfer = (args: TransferArgs): CompoundCommandCreator => ( volume: subTransferVol, labware: args.sourceLabware, well: sourceWell, - 'flow-rate': aspirateFlowRateUlSec, + flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, }), ...touchTipAfterAspirateCommands, @@ -225,11 +230,11 @@ const transfer = (args: TransferArgs): CompoundCommandCreator => ( volume: subTransferVol, labware: args.destLabware, well: destWell, - 'flow-rate': dispenseFlowRateUlSec, + flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, }), - ...touchTipAfterDispenseCommands, ...mixInDestinationCommands, + ...touchTipAfterDispenseCommands, ...blowoutCommand, ] diff --git a/protocol-designer/src/step-generation/getNextRobotStateAndWarnings/forAspirateDispense.js b/protocol-designer/src/step-generation/getNextRobotStateAndWarnings/forAspirateDispense.js index e5d83ef1bdc..65c1f988707 100644 --- a/protocol-designer/src/step-generation/getNextRobotStateAndWarnings/forAspirateDispense.js +++ b/protocol-designer/src/step-generation/getNextRobotStateAndWarnings/forAspirateDispense.js @@ -11,7 +11,10 @@ import { totalVolume, } from '../utils' import * as warningCreators from '../warningCreators' -import type { AspirateDispenseArgsV1 as AspirateDispenseArgs } from '@opentrons/shared-data' +import type { + AspirateParams, + DispenseParams, +} from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { InvariantContext, RobotState, @@ -28,7 +31,7 @@ type PipetteLiquidStateAcc = { } export default function getNextRobotStateAndWarningsForAspDisp( - args: AspirateDispenseArgs, + args: AspirateParams | DispenseParams, invariantContext: InvariantContext, prevRobotState: RobotState ): RobotStateAndWarnings { diff --git a/protocol-designer/src/step-generation/getNextRobotStateAndWarnings/index.js b/protocol-designer/src/step-generation/getNextRobotStateAndWarnings/index.js index dbbd280f460..be07c6f0cd8 100644 --- a/protocol-designer/src/step-generation/getNextRobotStateAndWarnings/index.js +++ b/protocol-designer/src/step-generation/getNextRobotStateAndWarnings/index.js @@ -1,7 +1,7 @@ // @flow import assert from 'assert' import forAspirateDispense from './forAspirateDispense' -import type { CommandV1 as Command } from '@opentrons/shared-data' +import type { Command } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { InvariantContext, RobotState, @@ -24,17 +24,17 @@ export default function getNextRobotStateAndWarnings( prevRobotState ) case 'blowout': - case 'drop-tip': - case 'pick-up-tip': + case 'dropTip': + case 'pickUpTip': // TODO: BC 2018-11-29 handle PipetteLabwareArgs return { robotState: prevRobotState, warnings: [] } - case 'touch-tip': - // TODO: BC 2018-11-29 handle touch-tip + case 'touchTip': + // TODO: BC 2018-11-29 handle touchTip return { robotState: prevRobotState, warnings: [] } case 'delay': // TODO: BC 2018-11-29 handle delay return { robotState: prevRobotState, warnings: [] } - case 'air-gap': + case 'airGap': // TODO: BC 2018-11-29 handle air-gap return { robotState: prevRobotState, warnings: [] } default: diff --git a/protocol-designer/src/step-generation/test-with-flow/aspirate.test.js b/protocol-designer/src/step-generation/test-with-flow/aspirate.test.js index b0cd153ac37..a67caf021bc 100644 --- a/protocol-designer/src/step-generation/test-with-flow/aspirate.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/aspirate.test.js @@ -1,6 +1,6 @@ // @flow import { expectTimelineError } from './testMatchers' -import _aspirate from '../commandCreators/atomic/aspirate' +import aspirate from '../commandCreators/atomic/aspirate' import { getLabwareDefURI } from '@opentrons/shared-data' import fixtureTipRack10Ul from '@opentrons/shared-data/labware/fixtures/2/fixtureTipRack10Ul.json' import fixtureTipRack1000Ul from '@opentrons/shared-data/labware/fixtures/2/fixtureTipRack1000Ul.json' @@ -8,17 +8,16 @@ import { getInitialRobotStateStandard, getRobotStateWithTipStandard, makeContext, - commandCreatorHasErrors, - commandCreatorNoErrors, + getSuccessResult, + getErrorResult, + DEFAULT_PIPETTE, + SOURCE_LABWARE, } from './fixtures' import getNextRobotStateAndWarnings from '../getNextRobotStateAndWarnings' jest.mock('../getNextRobotStateAndWarnings') jest.mock('../../labware-defs/utils') // TODO IMMEDIATELY move to somewhere more general -const aspirate = commandCreatorNoErrors(_aspirate) -const aspirateWithErrors = commandCreatorHasErrors(_aspirate) - const mockRobotStateAndWarningsReturnValue = { // using strings instead of properly-shaped objects for easier assertions robotState: 'expected robot state', @@ -36,82 +35,55 @@ describe('aspirate', () => { let initialRobotState let robotStateWithTip let invariantContext + let flowRateAndOffsets beforeEach(() => { invariantContext = makeContext() initialRobotState = getInitialRobotStateStandard(invariantContext) robotStateWithTip = getRobotStateWithTipStandard(invariantContext) + flowRateAndOffsets = { + flowRate: 6, + offsetFromBottomMm: 5, + } }) - describe('aspirate normally (with tip)', () => { - const optionalArgsCases = [ - { - description: 'no optional args', - expectInParams: false, - args: {}, - }, - { - description: 'null optional args', - expectInParams: false, - args: { - offsetFromBottomMm: null, - 'flow-rate': null, - }, - }, + test('aspirate normally (with tip)', () => { + const params = { + ...flowRateAndOffsets, + pipette: DEFAULT_PIPETTE, + volume: 50, + labware: SOURCE_LABWARE, + well: 'A1', + } + + const result = aspirate(params)(invariantContext, robotStateWithTip) + expect(getSuccessResult(result).commands).toEqual([ { - description: 'all optional args', - expectInParams: true, - args: { - offsetFromBottomMm: 5, - 'flow-rate': 6, - }, + command: 'aspirate', + params, }, - ] - - optionalArgsCases.forEach(testCase => { - test(testCase.description, () => { - const result = aspirate({ - pipette: 'p300SingleId', - volume: 50, - labware: 'sourcePlateId', - well: 'A1', - ...testCase.args, - })(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ - { - command: 'aspirate', - params: { - pipette: 'p300SingleId', - volume: 50, - labware: 'sourcePlateId', - well: 'A1', - ...(testCase.expectInParams ? testCase.args : {}), - }, - }, - ]) - }) - }) + ]) }) test('aspirate with volume > tip max volume should throw error', () => { invariantContext.pipetteEntities[ - 'p300SingleId' + DEFAULT_PIPETTE ].tiprackDefURI = getLabwareDefURI(fixtureTipRack10Ul) invariantContext.pipetteEntities[ - 'p300SingleId' + DEFAULT_PIPETTE ].tiprackLabwareDef = fixtureTipRack10Ul - const result = aspirateWithErrors({ - pipette: 'p300SingleId', + const result = aspirate({ + ...flowRateAndOffsets, + pipette: DEFAULT_PIPETTE, volume: 201, - labware: 'sourcePlateId', + labware: SOURCE_LABWARE, well: 'A1', })(invariantContext, robotStateWithTip) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ type: 'TIP_VOLUME_EXCEEDED', }) }) @@ -119,61 +91,65 @@ describe('aspirate', () => { test('aspirate with volume > pipette max volume should throw error', () => { // NOTE: assigning p300 to a 1000uL tiprack is nonsense, just for this test invariantContext.pipetteEntities[ - 'p300SingleId' + DEFAULT_PIPETTE ].tiprackDefURI = getLabwareDefURI(fixtureTipRack1000Ul) invariantContext.pipetteEntities[ - 'p300SingleId' + DEFAULT_PIPETTE ].tiprackLabwareDef = fixtureTipRack1000Ul - const result = aspirateWithErrors({ - pipette: 'p300SingleId', + const result = aspirate({ + ...flowRateAndOffsets, + pipette: DEFAULT_PIPETTE, volume: 301, - labware: 'sourcePlateId', + labware: SOURCE_LABWARE, well: 'A1', })(invariantContext, robotStateWithTip) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ type: 'PIPETTE_VOLUME_EXCEEDED', }) }) test('aspirate with invalid pipette ID should return error', () => { - const result = aspirateWithErrors({ + const result = aspirate({ + ...flowRateAndOffsets, pipette: 'badPipette', volume: 50, - labware: 'sourcePlateId', + labware: SOURCE_LABWARE, well: 'A1', })(invariantContext, robotStateWithTip) - expectTimelineError(result.errors, 'PIPETTE_DOES_NOT_EXIST') + expectTimelineError(getErrorResult(result).errors, 'PIPETTE_DOES_NOT_EXIST') }) test('aspirate with no tip should return error', () => { - const result = aspirateWithErrors({ - pipette: 'p300SingleId', + const result = aspirate({ + ...flowRateAndOffsets, + pipette: DEFAULT_PIPETTE, volume: 50, - labware: 'sourcePlateId', + labware: SOURCE_LABWARE, well: 'A1', })(invariantContext, initialRobotState) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ type: 'NO_TIP_ON_PIPETTE', }) }) test('aspirate from nonexistent labware should return error', () => { - const result = aspirateWithErrors({ - pipette: 'p300SingleId', + const result = aspirate({ + ...flowRateAndOffsets, + pipette: DEFAULT_PIPETTE, volume: 50, labware: 'problematicLabwareId', well: 'A1', })(invariantContext, robotStateWithTip) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ type: 'LABWARE_DOES_NOT_EXIST', }) }) @@ -181,22 +157,23 @@ describe('aspirate', () => { describe('liquid tracking', () => { test('aspirate calls getNextRobotStateAndWarnings with correct args and puts result into robotState', () => { const args = { - pipette: 'p300SingleId', - labware: 'sourcePlateId', + ...flowRateAndOffsets, + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, well: 'A1', volume: 152, } const result = aspirate(args)(invariantContext, robotStateWithTip) expect(getNextRobotStateAndWarnings).toHaveBeenCalledWith( - result.commands[0], + getSuccessResult(result).commands[0], invariantContext, robotStateWithTip ) - expect(result.robotState).toBe( + expect(getSuccessResult(result).robotState).toBe( mockRobotStateAndWarningsReturnValue.robotState ) - expect(result.warnings).toBe( + expect(getSuccessResult(result).warnings).toBe( mockRobotStateAndWarningsReturnValue.warnings ) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/blowout.test.js b/protocol-designer/src/step-generation/test-with-flow/blowout.test.js index 7cb2b7668b9..536d4a93b8c 100644 --- a/protocol-designer/src/step-generation/test-with-flow/blowout.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/blowout.test.js @@ -1,25 +1,25 @@ // @flow import { expectTimelineError } from './testMatchers' -import _blowout from '../commandCreators/atomic/blowout' +import blowout from '../commandCreators/atomic/blowout' import { makeContext, getInitialRobotStateStandard, getRobotStateWithTipStandard, - commandCreatorNoErrors, - commandCreatorHasErrors, + getErrorResult, + getSuccessResult, + DEFAULT_PIPETTE, + SOURCE_LABWARE, } from './fixtures' import updateLiquidState from '../dispenseUpdateLiquidState' -const blowout = commandCreatorNoErrors(_blowout) -const blowoutWithErrors = commandCreatorHasErrors(_blowout) - jest.mock('../dispenseUpdateLiquidState') describe('blowout', () => { let invariantContext let initialRobotState let robotStateWithTip + let params beforeEach(() => { invariantContext = makeContext() @@ -30,61 +30,58 @@ describe('blowout', () => { updateLiquidState.mockClear() // $FlowFixMe: mock methods updateLiquidState.mockReturnValue(initialRobotState.liquidState) + + params = { + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, + well: 'A1', + flowRate: 21.1, + offsetFromBottomMm: 1.3, + } }) test('blowout with tip', () => { - const result = blowout({ - pipette: 'p300SingleId', - labware: 'sourcePlateId', - well: 'A1', - })(invariantContext, robotStateWithTip) + const result = blowout(params)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ + const res = getSuccessResult(result) + expect(res.commands).toEqual([ { command: 'blowout', - params: { - pipette: 'p300SingleId', - labware: 'sourcePlateId', - well: 'A1', - }, + params, }, ]) - expect(result.robotState).toEqual(robotStateWithTip) + expect(res.robotState).toEqual(robotStateWithTip) }) test('blowout with invalid pipette ID should throw error', () => { - const result = blowoutWithErrors({ + const result = blowout({ + ...params, pipette: 'badPipette', - labware: 'sourcePlateId', - well: 'A1', })(invariantContext, robotStateWithTip) - expectTimelineError(result.errors, 'PIPETTE_DOES_NOT_EXIST') + expectTimelineError(getErrorResult(result).errors, 'PIPETTE_DOES_NOT_EXIST') }) test('blowout with invalid labware ID should throw error', () => { - const result = blowoutWithErrors({ - pipette: 'p300SingleId', + const result = blowout({ + ...params, labware: 'badLabware', - well: 'A1', })(invariantContext, robotStateWithTip) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + const res = getErrorResult(result) + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'LABWARE_DOES_NOT_EXIST', }) }) test('blowout with no tip should throw error', () => { - const result = blowoutWithErrors({ - pipette: 'p300SingleId', - labware: 'sourcePlateId', - well: 'A1', - })(invariantContext, initialRobotState) + const result = blowout(params)(invariantContext, initialRobotState) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + const res = getErrorResult(result) + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'NO_TIP_ON_PIPETTE', }) }) @@ -97,24 +94,22 @@ describe('blowout', () => { }) test('blowout calls dispenseUpdateLiquidState with max volume of pipette', () => { - const result = blowout({ - pipette: 'p300SingleId', - labware: 'sourcePlateId', - well: 'A1', - })(invariantContext, robotStateWithTip) + const result = blowout(params)(invariantContext, robotStateWithTip) expect(updateLiquidState).toHaveBeenCalledWith( { invariantContext, - pipetteId: 'p300SingleId', - labwareId: 'sourcePlateId', + pipetteId: DEFAULT_PIPETTE, + labwareId: SOURCE_LABWARE, useFullVolume: true, well: 'A1', }, robotStateWithTip.liquidState ) - expect(result.robotState.liquidState).toBe(mockLiquidReturnValue) + expect(getSuccessResult(result).robotState.liquidState).toBe( + mockLiquidReturnValue + ) }) }) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/blowoutUtil.test.js b/protocol-designer/src/step-generation/test-with-flow/blowoutUtil.test.js index 989ee84509a..38b58ed66ff 100644 --- a/protocol-designer/src/step-generation/test-with-flow/blowoutUtil.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/blowoutUtil.test.js @@ -5,25 +5,33 @@ import { SOURCE_WELL_BLOWOUT_DESTINATION, DEST_WELL_BLOWOUT_DESTINATION, } from '../utils' +import { + DEFAULT_PIPETTE, + SOURCE_LABWARE, + DEST_LABWARE, + TROUGH_LABWARE, + BLOWOUT_FLOW_RATE, + BLOWOUT_OFFSET_FROM_TOP_MM, + makeContext, +} from './fixtures' jest.mock('../commandCreators/atomic/blowout') -const pipetteId = 'p300SingleId' -const sourceLabwareId = 'sourcePlateId' -const sourceWell = 'A1' -const destLabwareId = 'destPlateId' -const destWell = 'A2' - -const blowoutArgs = [ - pipetteId, - sourceLabwareId, - sourceWell, - destLabwareId, - destWell, -] +let blowoutArgs describe('blowoutUtil', () => { beforeEach(() => { + blowoutArgs = { + pipette: DEFAULT_PIPETTE, + sourceLabwareId: SOURCE_LABWARE, + sourceWell: 'A1', + destLabwareId: DEST_LABWARE, + destWell: 'A2', + flowRate: BLOWOUT_FLOW_RATE, + offsetFromTopMm: BLOWOUT_OFFSET_FROM_TOP_MM, + invariantContext: makeContext(), + } + // $FlowFixMe _blowout.mockClear() // $FlowFixMe @@ -31,31 +39,52 @@ describe('blowoutUtil', () => { }) test('blowoutUtil calls blowout with source well params', () => { - blowoutUtil( + blowoutUtil({ ...blowoutArgs, - - SOURCE_WELL_BLOWOUT_DESTINATION - ) + blowoutLocation: SOURCE_WELL_BLOWOUT_DESTINATION, + }) expect(_blowout).toHaveBeenCalledWith({ - pipette: pipetteId, - labware: sourceLabwareId, - well: sourceWell, + pipette: blowoutArgs.pipette, + labware: blowoutArgs.sourceLabwareId, + well: blowoutArgs.sourceWell, + flowRate: blowoutArgs.flowRate, + offsetFromBottomMm: expect.any(Number), }) }) test('blowoutUtil calls blowout with dest plate params', () => { - blowoutUtil(...blowoutArgs, DEST_WELL_BLOWOUT_DESTINATION) + blowoutUtil({ + ...blowoutArgs, + blowoutLocation: DEST_WELL_BLOWOUT_DESTINATION, + }) + + expect(_blowout).toHaveBeenCalledWith({ + pipette: blowoutArgs.pipette, + labware: blowoutArgs.destLabwareId, + well: blowoutArgs.destWell, + flowRate: blowoutArgs.flowRate, + offsetFromBottomMm: expect.any(Number), + }) + }) + + test('blowoutUtil calls blowout with an arbitrary labware Id', () => { + blowoutUtil({ + ...blowoutArgs, + blowoutLocation: TROUGH_LABWARE, + }) expect(_blowout).toHaveBeenCalledWith({ - pipette: pipetteId, - labware: destLabwareId, - well: destWell, + pipette: blowoutArgs.pipette, + labware: TROUGH_LABWARE, + well: 'A1', + flowRate: blowoutArgs.flowRate, + offsetFromBottomMm: expect.any(Number), }) }) test('blowoutUtil returns an empty array if not given a blowoutLocation', () => { - const result = blowoutUtil(...blowoutArgs, null) + const result = blowoutUtil({ ...blowoutArgs, blowoutLocation: null }) expect(_blowout).not.toHaveBeenCalled() expect(result).toEqual([]) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/consolidate.test.js b/protocol-designer/src/step-generation/test-with-flow/consolidate.test.js index a324837690f..cb43126e1af 100644 --- a/protocol-designer/src/step-generation/test-with-flow/consolidate.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/consolidate.test.js @@ -8,28 +8,47 @@ import { makeContext, getTipColumn, getTiprackTipstate, - compoundCommandCreatorNoErrors, - compoundCommandCreatorHasErrors, - commandFixtures as cmd, + getSuccessResult, + getErrorResult, + DEFAULT_PIPETTE, + SOURCE_LABWARE, + DEST_LABWARE, + FIXED_TRASH_ID, + getFlowRateAndOffsetParams, + makeAspirateHelper, + makeDispenseHelper, + makeTouchTipHelper, + blowoutHelper, + pickUpTipHelper, + dropTipHelper, } from './fixtures' +import { reduceCommandCreators } from '../utils' import _consolidate from '../commandCreators/compound/consolidate' - -const consolidate = compoundCommandCreatorNoErrors(_consolidate) -const consolidateWithErrors = compoundCommandCreatorHasErrors(_consolidate) - -// shorthand -const dispense = (well, volume) => - cmd.dispense(well, volume, { labware: 'destPlateId' }) +import type { ConsolidateArgs } from '../types' + +const aspirateHelper = makeAspirateHelper() +const dispenseHelper = makeDispenseHelper() +const touchTipHelper = makeTouchTipHelper() +// TODO: Ian 2019-06-14 more elegant way to test the blowout offset calculation +const BLOWOUT_OFFSET_ANY: any = expect.any(Number) + +// collapse this compound command creator into the signature of an atomic command creator +const consolidate = (args: ConsolidateArgs) => ( + invariantContext, + initialRobotState +) => + reduceCommandCreators( + _consolidate(args)(invariantContext, initialRobotState) + )(invariantContext, initialRobotState) function tripleMix(well: string, volume: number, labware: string) { - const params = { labware } return [ - cmd.aspirate(well, volume, params), - cmd.dispense(well, volume, params), - cmd.aspirate(well, volume, params), - cmd.dispense(well, volume, params), - cmd.aspirate(well, volume, params), - cmd.dispense(well, volume, params), + aspirateHelper(well, volume, { labware }), + dispenseHelper(well, volume, { labware }), + aspirateHelper(well, volume, { labware }), + dispenseHelper(well, volume, { labware }), + aspirateHelper(well, volume, { labware }), + dispenseHelper(well, volume, { labware }), ] } @@ -39,6 +58,7 @@ let robotInitialStateNoLiquidState let robotStatePickedUpOneTipNoLiquidState let robotStatePickedUpMultiTipsNoLiquidState let robotStatePickedUpOneTip +let mixinArgs beforeEach(() => { invariantContext = makeContext() @@ -70,21 +90,21 @@ beforeEach(() => { }, } ) -}) -describe('consolidate single-channel', () => { - const baseData = { + mixinArgs = { + // `volume` and `changeTip` should be explicit in tests, + // those fields intentionally omitted from here + ...getFlowRateAndOffsetParams(), stepType: 'consolidate', + commandCreatorFnName: 'consolidate', name: 'Consolidate Test', description: 'test blah blah', - pipette: 'p300SingleId', + pipette: DEFAULT_PIPETTE, sourceWells: ['A1', 'A2', 'A3', 'A4'], destWell: 'B1', - sourceLabware: 'sourcePlateId', - destLabware: 'destPlateId', - - // volume and changeTip should be explicit in tests + sourceLabware: SOURCE_LABWARE, + destLabware: DEST_LABWARE, preWetTip: false, touchTipAfterAspirate: false, @@ -94,10 +114,12 @@ describe('consolidate single-channel', () => { mixInDestination: null, blowoutLocation: null, } +}) +describe('consolidate single-channel', () => { test('Minimal single-channel: A1 A2 to B1, 50uL with p300', () => { const data = { - ...baseData, + ...mixinArgs, sourceWells: ['A1', 'A2'], volume: 50, changeTip: 'once', @@ -105,65 +127,64 @@ describe('consolidate single-channel', () => { const result = consolidate(data)(invariantContext, initialRobotState) - expect(result.robotState).toMatchObject(robotStatePickedUpOneTip) + const res = getSuccessResult(result) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTip) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), - cmd.aspirate('A1', 50), - cmd.aspirate('A2', 50), - dispense('B1', 100), + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 50), + aspirateHelper('A2', 50), + dispenseHelper('B1', 100), ]) }) test('Single-channel with exceeding pipette max: A1 A2 A3 A4 to B1, 150uL with p300', () => { // TODO Ian 2018-05-03 is this a duplicate of exceeding max with changeTip="once"??? const data = { - ...baseData, + ...mixinArgs, volume: 150, changeTip: 'once', } const result = consolidate(data)(invariantContext, initialRobotState) - - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), - cmd.aspirate('A1', 150), - cmd.aspirate('A2', 150), - dispense('B1', 300), - - cmd.aspirate('A3', 150), - cmd.aspirate('A4', 150), - dispense('B1', 300), + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 150), + aspirateHelper('A2', 150), + dispenseHelper('B1', 300), + + aspirateHelper('A3', 150), + aspirateHelper('A4', 150), + dispenseHelper('B1', 300), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) test('Single-channel with exceeding pipette max: with changeTip="always"', () => { const data = { - ...baseData, + ...mixinArgs, volume: 150, changeTip: 'always', } const result = consolidate(data)(invariantContext, initialRobotState) - - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), - cmd.aspirate('A1', 150), - cmd.aspirate('A2', 150), - dispense('B1', 300), - cmd.dropTip('A1'), - - cmd.pickUpTip('B1'), - cmd.aspirate('A3', 150), - cmd.aspirate('A4', 150), - dispense('B1', 300), + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 150), + aspirateHelper('A2', 150), + dispenseHelper('B1', 300), + dropTipHelper('A1'), + + pickUpTipHelper('B1'), + aspirateHelper('A3', 150), + aspirateHelper('A4', 150), + dispenseHelper('B1', 300), ]) - expect(result.robotState).toMatchObject({ + expect(res.robotState).toMatchObject({ ...robotInitialStateNoLiquidState, tipState: { tipracks: { @@ -180,190 +201,183 @@ describe('consolidate single-channel', () => { test('Single-channel with exceeding pipette max: with changeTip="once"', () => { const data = { - ...baseData, + ...mixinArgs, volume: 150, changeTip: 'once', } const result = consolidate(data)(invariantContext, initialRobotState) - - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), - cmd.aspirate('A1', 150), - cmd.aspirate('A2', 150), - dispense('B1', 300), - - cmd.aspirate('A3', 150), - cmd.aspirate('A4', 150), - dispense('B1', 300), + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 150), + aspirateHelper('A2', 150), + dispenseHelper('B1', 300), + + aspirateHelper('A3', 150), + aspirateHelper('A4', 150), + dispenseHelper('B1', 300), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) test('Single-channel with exceeding pipette max: with changeTip="never"', () => { const data = { - ...baseData, + ...mixinArgs, volume: 150, changeTip: 'never', } const result = consolidate(data)(invariantContext, robotStatePickedUpOneTip) - - expect(result.commands).toEqual([ - cmd.aspirate('A1', 150), - cmd.aspirate('A2', 150), - dispense('B1', 300), - - cmd.aspirate('A3', 150), - cmd.aspirate('A4', 150), - dispense('B1', 300), + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + aspirateHelper('A1', 150), + aspirateHelper('A2', 150), + dispenseHelper('B1', 300), + + aspirateHelper('A3', 150), + aspirateHelper('A4', 150), + dispenseHelper('B1', 300), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) test('mix on aspirate should mix before aspirate in first well of chunk only', () => { const data = { - ...baseData, + ...mixinArgs, volume: 100, changeTip: 'once', mixFirstAspirate: { times: 3, volume: 50 }, } const result = consolidate(data)(invariantContext, initialRobotState) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), - ...tripleMix('A1', 50, 'sourcePlateId'), + ...tripleMix('A1', 50, SOURCE_LABWARE), - cmd.aspirate('A1', 100), - cmd.aspirate('A2', 100), - cmd.aspirate('A3', 100), - dispense('B1', 300), + aspirateHelper('A1', 100), + aspirateHelper('A2', 100), + aspirateHelper('A3', 100), + dispenseHelper('B1', 300), - ...tripleMix('A4', 50, 'sourcePlateId'), + ...tripleMix('A4', 50, SOURCE_LABWARE), - cmd.aspirate('A4', 100), - dispense('B1', 100), + aspirateHelper('A4', 100), + dispenseHelper('B1', 100), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) test('mix on aspirate', () => { const data = { - ...baseData, + ...mixinArgs, volume: 125, changeTip: 'once', mixFirstAspirate: { times: 3, volume: 50 }, } const result = consolidate(data)(invariantContext, initialRobotState) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), // Start mix - cmd.aspirate('A1', 50), - cmd.dispense('A1', 50), // sourceLabwareId - cmd.aspirate('A1', 50), - cmd.dispense('A1', 50), // sourceLabwareId - cmd.aspirate('A1', 50), - cmd.dispense('A1', 50), // sourceLabwareId + aspirateHelper('A1', 50), + dispenseHelper('A1', 50, { labware: SOURCE_LABWARE }), + aspirateHelper('A1', 50), + dispenseHelper('A1', 50, { labware: SOURCE_LABWARE }), + aspirateHelper('A1', 50), + dispenseHelper('A1', 50, { labware: SOURCE_LABWARE }), // done mix - cmd.aspirate('A1', 125), - cmd.aspirate('A2', 125), - dispense('B1', 250), + aspirateHelper('A1', 125), + aspirateHelper('A2', 125), + dispenseHelper('B1', 250), // Start mix - cmd.aspirate('A3', 50), - cmd.dispense('A3', 50), // sourceLabwareId - cmd.aspirate('A3', 50), - cmd.dispense('A3', 50), // sourceLabwareId - cmd.aspirate('A3', 50), - cmd.dispense('A3', 50), // sourceLabwareId + aspirateHelper('A3', 50), + dispenseHelper('A3', 50, { labware: SOURCE_LABWARE }), + aspirateHelper('A3', 50), + dispenseHelper('A3', 50, { labware: SOURCE_LABWARE }), + aspirateHelper('A3', 50), + dispenseHelper('A3', 50, { labware: SOURCE_LABWARE }), // done mix - cmd.aspirate('A3', 125), - cmd.aspirate('A4', 125), - dispense('B1', 250), + aspirateHelper('A3', 125), + aspirateHelper('A4', 125), + dispenseHelper('B1', 250), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) test('mix after dispense', () => { const data = { - ...baseData, + ...mixinArgs, volume: 100, changeTip: 'once', mixInDestination: { times: 3, volume: 53 }, } const result = consolidate(data)(invariantContext, initialRobotState) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), - cmd.aspirate('A1', 100), - cmd.aspirate('A2', 100), - cmd.aspirate('A3', 100), - dispense('B1', 300), + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 100), + aspirateHelper('A2', 100), + aspirateHelper('A3', 100), + dispenseHelper('B1', 300), - ...tripleMix('B1', 53, 'destPlateId'), + ...tripleMix('B1', 53, DEST_LABWARE), - cmd.aspirate('A4', 100), - dispense('B1', 100), + aspirateHelper('A4', 100), + dispenseHelper('B1', 100), - ...tripleMix('B1', 53, 'destPlateId'), + ...tripleMix('B1', 53, DEST_LABWARE), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) test('mix after dispense with blowout to trash: first mix, then blowout', () => { const data = { - ...baseData, + ...mixinArgs, volume: 100, changeTip: 'once', mixInDestination: { times: 3, volume: 54 }, - blowoutLocation: 'trashId', + blowoutLocation: FIXED_TRASH_ID, } const result = consolidate(data)(invariantContext, initialRobotState) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), - cmd.aspirate('A1', 100), - cmd.aspirate('A2', 100), - cmd.aspirate('A3', 100), - dispense('B1', 300), + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 100), + aspirateHelper('A2', 100), + aspirateHelper('A3', 100), + dispenseHelper('B1', 300), - ...tripleMix('B1', 54, 'destPlateId'), + ...tripleMix('B1', 54, DEST_LABWARE), - cmd.blowout(), - cmd.aspirate('A4', 100), - dispense('B1', 100), + blowoutHelper(null, { offsetFromBottomMm: BLOWOUT_OFFSET_ANY }), + aspirateHelper('A4', 100), + dispenseHelper('B1', 100), - ...tripleMix('B1', 54, 'destPlateId'), + ...tripleMix('B1', 54, DEST_LABWARE), - cmd.blowout(), + blowoutHelper(null, { offsetFromBottomMm: BLOWOUT_OFFSET_ANY }), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) test('"pre-wet tip" should aspirate and dispense consolidate volume from first well of each chunk', () => { // TODO LATER Ian 2018-02-13 Should it be 2/3 max volume instead? const data = { - ...baseData, + ...mixinArgs, volume: 150, changeTip: 'once', preWetTip: true, @@ -373,112 +387,115 @@ describe('consolidate single-channel', () => { const preWetVol = data.volume // NOTE same as volume above... for now const result = consolidate(data)(invariantContext, initialRobotState) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), // pre-wet tip - cmd.aspirate('A1', preWetVol), - cmd.dispense('A1', preWetVol), + aspirateHelper('A1', preWetVol), + dispenseHelper('A1', preWetVol, { labware: SOURCE_LABWARE }), // done pre-wet - cmd.aspirate('A1', 150), - cmd.aspirate('A2', 150), - dispense('B1', 300), + aspirateHelper('A1', 150), + aspirateHelper('A2', 150), + dispenseHelper('B1', 300), // pre-wet tip, now with A3 - cmd.aspirate('A3', preWetVol), - cmd.dispense('A3', preWetVol), + aspirateHelper('A3', preWetVol), + dispenseHelper('A3', preWetVol, { labware: SOURCE_LABWARE }), // done pre-wet - cmd.aspirate('A3', 150), - cmd.aspirate('A4', 150), - dispense('B1', 300), + aspirateHelper('A3', 150), + aspirateHelper('A4', 150), + dispenseHelper('B1', 300), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) - test('touch-tip after aspirate should touch tip after every aspirate command', () => { + test('touchTip after aspirate should touch tip after every aspirate command', () => { const data = { - ...baseData, + ...mixinArgs, volume: 150, changeTip: 'once', touchTipAfterAspirate: true, } const result = consolidate(data)(invariantContext, initialRobotState) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), + const touchTipAfterAsp = { + offsetFromBottomMm: mixinArgs.touchTipAfterAspirateOffsetMmFromBottom, + } + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), - cmd.aspirate('A1', 150), - cmd.touchTip('A1'), + aspirateHelper('A1', 150), + touchTipHelper('A1', touchTipAfterAsp), - cmd.aspirate('A2', 150), - cmd.touchTip('A2'), + aspirateHelper('A2', 150), + touchTipHelper('A2', touchTipAfterAsp), - dispense('B1', 300), + dispenseHelper('B1', 300), - cmd.aspirate('A3', 150), - cmd.touchTip('A3'), + aspirateHelper('A3', 150), + touchTipHelper('A3', touchTipAfterAsp), - cmd.aspirate('A4', 150), - cmd.touchTip('A4'), + aspirateHelper('A4', 150), + touchTipHelper('A4', touchTipAfterAsp), - dispense('B1', 300), + dispenseHelper('B1', 300), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) - test('touch-tip after dispense should touch tip after dispense on destination well', () => { + test('touchTip after dispense should touch tip after dispense on destination well', () => { const data = { - ...baseData, + ...mixinArgs, volume: 150, changeTip: 'once', touchTipAfterDispense: true, } const result = consolidate(data)(invariantContext, initialRobotState) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), + const touchTipAfterDisp = { + labware: DEST_LABWARE, + offsetFromBottomMm: mixinArgs.touchTipAfterDispenseOffsetMmFromBottom, + } + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), - cmd.aspirate('A1', 150), - cmd.aspirate('A2', 150), + aspirateHelper('A1', 150), + aspirateHelper('A2', 150), - dispense('B1', 300), - cmd.touchTip('B1', { labware: 'destPlateId' }), + dispenseHelper('B1', 300), + touchTipHelper('B1', touchTipAfterDisp), - cmd.aspirate('A3', 150), - cmd.aspirate('A4', 150), + aspirateHelper('A3', 150), + aspirateHelper('A4', 150), - dispense('B1', 300), - cmd.touchTip('B1', { labware: 'destPlateId' }), + dispenseHelper('B1', 300), + touchTipHelper('B1', touchTipAfterDisp), ]) - expect(result.robotState).toMatchObject( - robotStatePickedUpOneTipNoLiquidState - ) + expect(res.robotState).toMatchObject(robotStatePickedUpOneTipNoLiquidState) }) test('invalid pipette ID should return error', () => { const data = { - ...baseData, + ...mixinArgs, sourceWells: ['A1', 'A2'], volume: 150, changeTip: 'once', pipette: 'no-such-pipette-id-here', } - const result = consolidateWithErrors(data)( - invariantContext, - initialRobotState - ) + const result = consolidate(data)(invariantContext, initialRobotState) + const res = getErrorResult(result) - expect(result.errors).toHaveLength(1) - expect(result.errors[0].type).toEqual('PIPETTE_DOES_NOT_EXIST') + expect(res.errors).toHaveLength(1) + expect(res.errors[0].type).toEqual('PIPETTE_DOES_NOT_EXIST') }) test.skip('air gap', () => {}) // TODO Ian 2018-04-05 determine air gap behavior @@ -487,21 +504,22 @@ describe('consolidate single-channel', () => { describe('consolidate multi-channel', () => { const multiParams = { pipette: 'p300MultiId' } const multiDispense = (well: string, volume: number) => - cmd.dispense(well, volume, { - labware: 'destPlateId', + dispenseHelper(well, volume, { + labware: DEST_LABWARE, pipette: 'p300MultiId', }) - const baseData = { + const args = { stepType: 'consolidate', + commandCreatorFnName: 'consolidate', name: 'Consolidate Test', description: 'test blah blah', pipette: 'p300MultiId', sourceWells: ['A1', 'A2', 'A3', 'A4'], destWell: 'A12', - sourceLabware: 'sourcePlateId', - destLabware: 'destPlateId', + sourceLabware: SOURCE_LABWARE, + destLabware: DEST_LABWARE, // volume and changeTip should be explicit in tests @@ -512,27 +530,30 @@ describe('consolidate multi-channel', () => { touchTipAfterDispense: false, mixInDestination: null, blowoutLocation: null, + + ...getFlowRateAndOffsetParams(), } test('simple multi-channel: cols A1 A2 A3 A4 to col A12', () => { const data = { - ...baseData, + ...args, volume: 140, changeTip: 'once', } const result = consolidate(data)(invariantContext, initialRobotState) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1', multiParams), - cmd.aspirate('A1', 140, multiParams), - cmd.aspirate('A2', 140, multiParams), + expect(res.commands).toEqual([ + pickUpTipHelper('A1', multiParams), + aspirateHelper('A1', 140, multiParams), + aspirateHelper('A2', 140, multiParams), multiDispense('A12', 280), - cmd.aspirate('A3', 140, multiParams), - cmd.aspirate('A4', 140, multiParams), + aspirateHelper('A3', 140, multiParams), + aspirateHelper('A4', 140, multiParams), multiDispense('A12', 280), ]) - expect(result.robotState).toMatchObject( + expect(res.robotState).toMatchObject( robotStatePickedUpMultiTipsNoLiquidState ) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/delay.test.js b/protocol-designer/src/step-generation/test-with-flow/delay.test.js index 72e5d28a34e..8f325a6f665 100644 --- a/protocol-designer/src/step-generation/test-with-flow/delay.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/delay.test.js @@ -1,8 +1,6 @@ // @flow -import _delay from '../commandCreators/atomic/delay' -import { commandCreatorNoErrors } from './fixtures' - -const delay = commandCreatorNoErrors(_delay) +import delay from '../commandCreators/atomic/delay' +import { getSuccessResult } from './fixtures' const getRobotInitialState = (): any => { // This particular state shouldn't matter for delay @@ -19,15 +17,15 @@ describe('delay indefinitely', () => { const result = delay({ message, - description: 'description', - name: 'name', wait: true, })(invariantContext, robotInitialState) - expect(result.robotState).toEqual(getRobotInitialState()) - expect(result.robotState).toBe(robotInitialState) // same object + const res = getSuccessResult(result) + + expect(res.robotState).toEqual(getRobotInitialState()) + expect(res.robotState).toBe(robotInitialState) // same object - expect(result.commands).toEqual([ + expect(res.commands).toEqual([ { command: 'delay', params: { @@ -46,15 +44,15 @@ describe('delay for a given time', () => { const result = delay({ message, - description: 'description', - name: 'name', wait: 95.5, })(invariantContext, robotInitialState) - expect(result.robotState).toEqual(getRobotInitialState()) - expect(result.robotState).toBe(robotInitialState) // same object + const res = getSuccessResult(result) + + expect(res.robotState).toEqual(getRobotInitialState()) + expect(res.robotState).toBe(robotInitialState) // same object - expect(result.commands).toEqual([ + expect(res.commands).toEqual([ { command: 'delay', params: { diff --git a/protocol-designer/src/step-generation/test-with-flow/dispense.test.js b/protocol-designer/src/step-generation/test-with-flow/dispense.test.js index f95e0b4e70f..7466a81a1e5 100644 --- a/protocol-designer/src/step-generation/test-with-flow/dispense.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/dispense.test.js @@ -3,19 +3,18 @@ import { getInitialRobotStateStandard, getRobotStateWithTipStandard, makeContext, - commandCreatorNoErrors, - commandCreatorHasErrors, + getErrorResult, + getSuccessResult, + DEFAULT_PIPETTE, + SOURCE_LABWARE, } from './fixtures' -import _dispense from '../commandCreators/atomic/dispense' +import dispense from '../commandCreators/atomic/dispense' import updateLiquidState from '../dispenseUpdateLiquidState' jest.mock('../dispenseUpdateLiquidState') jest.mock('../../labware-defs/utils') // TODO IMMEDIATELY move to somewhere more general -const dispense = commandCreatorNoErrors(_dispense) -const dispenseWithErrors = commandCreatorHasErrors(_dispense) - describe('dispense', () => { let initialRobotState let robotStateWithTip @@ -33,100 +32,47 @@ describe('dispense', () => { }) describe('tip tracking & commands:', () => { - describe('dispense normally (with tip)', () => { - const optionalArgsCases = [ - { - description: 'no optional args', - expectInParams: false, - args: {}, - }, - { - description: 'null optional args', - expectInParams: false, - args: { - offsetFromBottomMm: null, - 'flow-rate': null, - }, - }, - { - description: 'all optional args', - expectInParams: true, - args: { - offsetFromBottomMm: 5, - 'flow-rate': 6, - }, - }, - ] - optionalArgsCases.forEach(testCase => { - test(testCase.description, () => { - const result = dispense({ - pipette: 'p300SingleId', - volume: 50, - labware: 'sourcePlateId', - well: 'A1', - ...testCase.args, - })(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ - { - command: 'dispense', - params: { - pipette: 'p300SingleId', - volume: 50, - labware: 'sourcePlateId', - well: 'A1', - ...(testCase.expectInParams ? testCase.args : {}), - }, - }, - ]) - }) - }) - }) - - test('dispense normally (with tip) and optional args', () => { - const args = { - pipette: 'p300SingleId', + let params + beforeEach(() => { + params = { + pipette: DEFAULT_PIPETTE, volume: 50, - labware: 'sourcePlateId', + labware: SOURCE_LABWARE, well: 'A1', offsetFromBottomMm: 5, - 'flow-rate': 6, + flowRate: 6, } + }) + test('dispense normally (with tip)', () => { + const result = dispense(params)(invariantContext, robotStateWithTip) - const result = dispense(args)(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ + expect(getSuccessResult(result).commands).toEqual([ { command: 'dispense', - params: args, + params, }, ]) }) test('dispensing without tip should throw error', () => { - const result = dispenseWithErrors({ - pipette: 'p300SingleId', - volume: 50, - labware: 'sourcePlateId', - well: 'A1', - })(invariantContext, initialRobotState) + const result = dispense(params)(invariantContext, initialRobotState) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + const res = getErrorResult(result) + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'NO_TIP_ON_PIPETTE', }) }) test('dispense to nonexistent labware should throw error', () => { - const result = dispenseWithErrors({ - pipette: 'p300SingleId', - volume: 50, + const result = dispense({ + ...params, labware: 'someBadLabwareId', - well: 'A1', })(invariantContext, robotStateWithTip) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + const res = getErrorResult(result) + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'LABWARE_DOES_NOT_EXIST', }) }) @@ -145,25 +91,30 @@ describe('dispense', () => { }) test('dispense calls dispenseUpdateLiquidState with correct args and puts result into robotState.liquidState', () => { - const result = dispense({ - pipette: 'p300SingleId', - labware: 'sourcePlateId', + const params = { + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, well: 'A1', volume: 152, - })(invariantContext, robotStateWithTip) + flowRate: 12, + offsetFromBottomMm: 21, + } + const result = dispense(params)(invariantContext, robotStateWithTip) expect(updateLiquidState).toHaveBeenCalledWith( { invariantContext, - pipetteId: 'p300SingleId', - labwareId: 'sourcePlateId', - volume: 152, - well: 'A1', + pipetteId: params.pipette, + labwareId: params.labware, + volume: params.volume, + well: params.well, }, robotStateWithTip.liquidState ) - expect(result.robotState.liquidState).toBe(mockLiquidReturnValue) + expect(getSuccessResult(result).robotState.liquidState).toBe( + mockLiquidReturnValue + ) }) }) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/dispenseUpdateLiquidState.test.js b/protocol-designer/src/step-generation/test-with-flow/dispenseUpdateLiquidState.test.js index 90aeb325d66..68b5b3135ca 100644 --- a/protocol-designer/src/step-generation/test-with-flow/dispenseUpdateLiquidState.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/dispenseUpdateLiquidState.test.js @@ -6,7 +6,7 @@ import fixture384Plate from '@opentrons/shared-data/labware/fixtures/2/fixture38 import merge from 'lodash/merge' import omit from 'lodash/omit' import { createEmptyLiquidState, createTipLiquidState } from '../utils' -import { makeContext } from './fixtures' +import { makeContext, DEFAULT_PIPETTE, SOURCE_LABWARE } from './fixtures' import _updateLiquidState from '../dispenseUpdateLiquidState' @@ -17,9 +17,9 @@ beforeEach(() => { invariantContext = makeContext() dispenseSingleCh150ToA1Args = { invariantContext, - pipetteId: 'p300SingleId', + pipetteId: DEFAULT_PIPETTE, volume: 150, - labwareId: 'sourcePlateId', + labwareId: SOURCE_LABWARE, well: 'A1', } }) @@ -334,7 +334,7 @@ describe('...8-channel pipette', () => { test(labwareType, () => { let customInvariantContext = makeContext() customInvariantContext.labwareEntities.sourcePlateId = { - id: 'sourcePlateId', + id: SOURCE_LABWARE, labwareDefURI: labwareType, def, } @@ -365,7 +365,7 @@ describe('...8-channel pipette', () => { invariantContext: customInvariantContext, pipetteId: 'p300MultiId', volume: 150, - labwareId: 'sourcePlateId', + labwareId: SOURCE_LABWARE, well: 'A1', }, initialLiquidState diff --git a/protocol-designer/src/step-generation/test-with-flow/distribute.test.js b/protocol-designer/src/step-generation/test-with-flow/distribute.test.js index f10af50a40c..506de01d39a 100644 --- a/protocol-designer/src/step-generation/test-with-flow/distribute.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/distribute.test.js @@ -1,21 +1,41 @@ // @flow -import _distribute from '../commandCreators/compound/distribute' -// import merge from 'lodash/merge' import { getRobotInitialStateNoTipsRemain, getRobotStateWithTipStandard, makeContext, - compoundCommandCreatorNoErrors, - compoundCommandCreatorHasErrors, - commandFixtures as cmd, + getSuccessResult, + getErrorResult, + DEFAULT_PIPETTE, + SOURCE_LABWARE, + DEST_LABWARE, + FIXED_TRASH_ID, + getFlowRateAndOffsetParams, + makeAspirateHelper, + makeDispenseHelper, + blowoutHelper, + makeTouchTipHelper, + pickUpTipHelper, + dropTipHelper, } from './fixtures' +import { reduceCommandCreators } from '../utils' +import _distribute from '../commandCreators/compound/distribute' import type { DistributeArgs } from '../types' -const distribute = compoundCommandCreatorNoErrors(_distribute) -const distributeWithErrors = compoundCommandCreatorHasErrors(_distribute) -// shorthand -const dispense = (well, volume) => - cmd.dispense(well, volume, { labware: 'destPlateId' }) +const aspirateHelper = makeAspirateHelper() +const dispenseHelper = makeDispenseHelper() +const touchTipHelper = makeTouchTipHelper() +// TODO: Ian 2019-06-14 more elegant way to test the blowout offset calculation +const BLOWOUT_OFFSET_ANY: any = expect.any(Number) + +// collapse this compound command creator into the signature of an atomic command creator +const distribute = (args: DistributeArgs) => ( + invariantContext, + initialRobotState +) => + reduceCommandCreators(_distribute(args)(invariantContext, initialRobotState))( + invariantContext, + initialRobotState + ) let mixinArgs let invariantContext @@ -26,26 +46,31 @@ let blowoutSingleToSourceA1 beforeEach(() => { mixinArgs = { + ...getFlowRateAndOffsetParams(), commandCreatorFnName: 'distribute', name: 'distribute test', description: 'test blah blah', - pipette: 'p300SingleId', - sourceLabware: 'sourcePlateId', - destLabware: 'destPlateId', + pipette: DEFAULT_PIPETTE, + sourceLabware: SOURCE_LABWARE, + destLabware: DEST_LABWARE, preWetTip: false, touchTipAfterAspirate: false, disposalVolume: 60, - disposalLabware: 'trashId', + disposalLabware: FIXED_TRASH_ID, disposalWell: 'A1', mixBeforeAspirate: null, touchTipAfterDispense: false, } - blowoutSingleToTrash = cmd.blowout('trashId') - blowoutSingleToSourceA1 = cmd.blowout('sourcePlateId', { well: 'A1' }) + blowoutSingleToTrash = blowoutHelper(FIXED_TRASH_ID, { + offsetFromBottomMm: BLOWOUT_OFFSET_ANY, + }) + blowoutSingleToSourceA1 = blowoutHelper(SOURCE_LABWARE, { + offsetFromBottomMm: BLOWOUT_OFFSET_ANY, + }) invariantContext = makeContext() robotStateWithTip = getRobotStateWithTipStandard(invariantContext) @@ -56,9 +81,7 @@ beforeEach(() => { describe('distribute: minimal example', () => { test('single channel; 60uL from A1 -> A2, A3; no tip pickup', () => { - // TODO Ian 2018-05-03 distributeArgs needs to be typed because the - // commandCreatorNoErrors wrapper casts the arg type to any :( - const distributeArgs: DistributeArgs = { + const distributeArgs = { ...mixinArgs, sourceWell: 'A1', destWells: ['A2', 'A3'], @@ -69,11 +92,11 @@ describe('distribute: minimal example', () => { invariantContext, robotStateWithTip ) - - expect(result.commands).toEqual([ - cmd.aspirate('A1', 180), - dispense('A2', 60), - dispense('A3', 60), + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + aspirateHelper('A1', 180), + dispenseHelper('A2', 60), + dispenseHelper('A3', 60), blowoutSingleToTrash, ]) }) @@ -93,18 +116,19 @@ describe('tip handling for multiple distribute chunks', () => { invariantContext, robotStateWithTip ) - - expect(result.commands).toEqual([ - cmd.dropTip('A1'), - cmd.pickUpTip('A1'), - cmd.aspirate('A1', 240), - dispense('A2', 90), - dispense('A3', 90), + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + dropTipHelper('A1'), + pickUpTipHelper('A1'), + aspirateHelper('A1', 240), + dispenseHelper('A2', 90), + dispenseHelper('A3', 90), blowoutSingleToTrash, - cmd.aspirate('A1', 240), - dispense('A4', 90), - dispense('A5', 90), + aspirateHelper('A1', 240), + dispenseHelper('A4', 90), + dispenseHelper('A5', 90), blowoutSingleToTrash, ]) @@ -123,21 +147,22 @@ describe('tip handling for multiple distribute chunks', () => { invariantContext, robotStateWithTip ) - - expect(result.commands).toEqual([ - cmd.dropTip('A1'), - cmd.pickUpTip('A1'), - cmd.aspirate('A1', 240), - dispense('A2', 90), - dispense('A3', 90), + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + dropTipHelper('A1'), + pickUpTipHelper('A1'), + aspirateHelper('A1', 240), + dispenseHelper('A2', 90), + dispenseHelper('A3', 90), blowoutSingleToTrash, // next chunk, change tip - cmd.dropTip('A1'), - cmd.pickUpTip('B1'), - cmd.aspirate('A1', 240), - dispense('A4', 90), - dispense('A5', 90), + dropTipHelper('A1'), + pickUpTipHelper('B1'), + aspirateHelper('A1', 240), + dispenseHelper('A4', 90), + dispenseHelper('A5', 90), blowoutSingleToTrash, ]) }) @@ -155,15 +180,16 @@ describe('tip handling for multiple distribute chunks', () => { invariantContext, robotStateWithTip ) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ - cmd.aspirate('A1', 240), - dispense('A2', 90), - dispense('A3', 90), + expect(res.commands).toEqual([ + aspirateHelper('A1', 240), + dispenseHelper('A2', 90), + dispenseHelper('A3', 90), blowoutSingleToTrash, - cmd.aspirate('A1', 240), - dispense('A4', 90), - dispense('A5', 90), + aspirateHelper('A1', 240), + dispenseHelper('A4', 90), + dispenseHelper('A5', 90), blowoutSingleToTrash, ]) }) @@ -177,13 +203,14 @@ describe('tip handling for multiple distribute chunks', () => { volume: 150, } - const result = distributeWithErrors(distributeArgs)( + const result = distribute(distributeArgs)( invariantContext, robotInitialStateNoTipsRemain ) + const res = getErrorResult(result) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'INSUFFICIENT_TIPS', }) }) @@ -202,28 +229,29 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch', () => { mixFirstAspirate: true, disposalVolume: 12, - disposalLabware: 'sourcePlateId', + disposalLabware: SOURCE_LABWARE, disposalWell: 'A1', } const result = distribute(distributeArgs)( invariantContext, robotStateWithTip ) + const res = getSuccessResult(result) const aspirateVol = 120 * 2 + 12 - expect(result.commands).toEqual([ - cmd.aspirate('A1', aspirateVol), - dispense('A2', 120), - dispense('A3', 120), + expect(res.commands).toEqual([ + aspirateHelper('A1', aspirateVol), + dispenseHelper('A2', 120), + dispenseHelper('A3', 120), blowoutSingleToSourceA1, - cmd.aspirate('A1', aspirateVol), - dispense('A4', 120), - dispense('A5', 120), + aspirateHelper('A1', aspirateVol), + dispenseHelper('A4', 120), + dispenseHelper('A5', 120), blowoutSingleToSourceA1, - cmd.aspirate('A1', 120 + 12), - dispense('A6', 120), + aspirateHelper('A1', 120 + 12), + dispenseHelper('A6', 120), blowoutSingleToSourceA1, ]) }) @@ -242,71 +270,25 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch', () => { invariantContext, robotStateWithTip ) + const res = getSuccessResult(result) const preWetVolume = 42 // TODO what is pre-wet volume? + const preWetTipCommands = [ - { - command: 'aspirate', - labware: 'sourcePlateId', - pipette: 'p300SingleId', - volume: preWetVolume, - well: 'A1', - }, - { - command: 'dispense', - labware: 'sourcePlateId', - pipette: 'p300SingleId', - volume: preWetVolume, - well: 'A1', - }, + aspirateHelper('A1', preWetVolume), + dispenseHelper('A1', preWetVolume, { labware: SOURCE_LABWARE }), ] - expect(result.commands).toEqual([ + expect(res.commands).toEqual([ ...preWetTipCommands, - { - command: 'aspirate', - labware: 'sourcePlateId', - pipette: 'p300SingleId', - volume: 300, - well: 'A1', - }, - { - command: 'dispense', - labware: 'destPlateId', - pipette: 'p300SingleId', - volume: 150, - well: 'A2', - }, - { - command: 'dispense', - labware: 'destPlateId', - pipette: 'p300SingleId', - volume: 150, - well: 'A3', - }, + aspirateHelper('A1', 300), + dispenseHelper('A2', 150), + dispenseHelper('A3', 150), blowoutSingleToTrash, ...preWetTipCommands, - { - command: 'aspirate', - labware: 'sourcePlateId', - pipette: 'p300SingleId', - volume: 300, - well: 'A1', - }, - { - command: 'dispense', - labware: 'destPlateId', - pipette: 'p300SingleId', - volume: 150, - well: 'A4', - }, - { - command: 'dispense', - labware: 'destPlateId', - pipette: 'p300SingleId', - volume: 150, - well: 'A5', - }, + aspirateHelper('A1', 300), + dispenseHelper('A4', 150), + dispenseHelper('A5', 150), blowoutSingleToTrash, ]) }) @@ -324,18 +306,19 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch', () => { invariantContext, robotStateWithTip ) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ - cmd.aspirate('A1', 240), - cmd.touchTip('A1'), - dispense('A2', 90), - dispense('A3', 90), + expect(res.commands).toEqual([ + aspirateHelper('A1', 240), + touchTipHelper('A1'), + dispenseHelper('A2', 90), + dispenseHelper('A3', 90), blowoutSingleToTrash, - cmd.aspirate('A1', 240), - cmd.touchTip('A1'), - dispense('A4', 90), - dispense('A5', 90), + aspirateHelper('A1', 240), + touchTipHelper('A1'), + dispenseHelper('A4', 90), + dispenseHelper('A5', 90), blowoutSingleToTrash, ]) }) @@ -353,24 +336,21 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch', () => { invariantContext, robotStateWithTip ) - - function touchTip(well: string) { - return cmd.touchTip(well, { labware: 'destPlateId' }) - } - - expect(result.commands).toEqual([ - cmd.aspirate('A1', 240), - dispense('A2', 90), - touchTip('A2'), - dispense('A3', 90), - touchTip('A3'), + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + aspirateHelper('A1', 240), + dispenseHelper('A2', 90), + touchTipHelper('A2', { labware: DEST_LABWARE }), + dispenseHelper('A3', 90), + touchTipHelper('A3', { labware: DEST_LABWARE }), blowoutSingleToTrash, - cmd.aspirate('A1', 240), - dispense('A4', 90), - touchTip('A4'), - dispense('A5', 90), - touchTip('A5'), + aspirateHelper('A1', 240), + dispenseHelper('A4', 90), + touchTipHelper('A4', { labware: DEST_LABWARE }), + dispenseHelper('A5', 90), + touchTipHelper('A5', { labware: DEST_LABWARE }), blowoutSingleToTrash, ]) }) @@ -378,7 +358,7 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch', () => { test('mix before aspirate w/ disposal vol', () => { const volume = 130 const disposalVolume = 20 - const disposalLabware = 'sourcePlateId' + const disposalLabware = SOURCE_LABWARE const disposalWell = 'A1' const aspirateVol = volume * 2 + disposalVolume const distributeArgs: DistributeArgs = { @@ -400,27 +380,28 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch', () => { invariantContext, robotStateWithTip ) + const res = getSuccessResult(result) const mixCommands = [ // mix 1 - cmd.aspirate('A1', 250), - cmd.dispense('A1', 250), // dispense to sourcePlateId + aspirateHelper('A1', 250), + dispenseHelper('A1', 250, { labware: SOURCE_LABWARE }), // mix 2 - cmd.aspirate('A1', 250), - cmd.dispense('A1', 250), // dispense to sourcePlateId + aspirateHelper('A1', 250), + dispenseHelper('A1', 250, { labware: SOURCE_LABWARE }), ] - expect(result.commands).toEqual([ + expect(res.commands).toEqual([ ...mixCommands, - cmd.aspirate('A1', aspirateVol), - dispense('A2', volume), - dispense('A3', volume), + aspirateHelper('A1', aspirateVol), + dispenseHelper('A2', volume), + dispenseHelper('A3', volume), blowoutSingleToSourceA1, ...mixCommands, - cmd.aspirate('A1', aspirateVol), - dispense('A4', volume), - dispense('A5', volume), + aspirateHelper('A1', aspirateVol), + dispenseHelper('A4', volume), + dispenseHelper('A5', volume), blowoutSingleToSourceA1, ]) }) @@ -437,13 +418,14 @@ describe('invalid input + state errors', () => { pipette: 'no-such-pipette-id-here', } - const result = distributeWithErrors(distributeArgs)( + const result = distribute(distributeArgs)( invariantContext, robotStateWithTip ) + const res = getErrorResult(result) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'PIPETTE_DOES_NOT_EXIST', }) }) @@ -462,13 +444,14 @@ describe('distribute volume exceeds pipette max volume', () => { disposalLabware: null, disposalWell: null, } - const result = distributeWithErrors(distributeArgs)( + const result = distribute(distributeArgs)( invariantContext, robotStateWithTip ) + const res = getErrorResult(result) - expect(result.errors).toHaveLength(1) - expect(result.errors[0].type).toEqual('PIPETTE_VOLUME_EXCEEDED') + expect(res.errors).toHaveLength(1) + expect(res.errors[0].type).toEqual('PIPETTE_VOLUME_EXCEEDED') }) test(`with disposal volume`, () => { @@ -480,15 +463,16 @@ describe('distribute volume exceeds pipette max volume', () => { changeTip, volume: 250, disposalVolume: 100, - disposalLabware: 'trashId', + disposalLabware: FIXED_TRASH_ID, disposalWell: 'A1', } - const result = distributeWithErrors(distributeArgs)( + const result = distribute(distributeArgs)( invariantContext, robotStateWithTip ) + const res = getErrorResult(result) - expect(result.errors).toHaveLength(1) - expect(result.errors[0].type).toEqual('PIPETTE_VOLUME_EXCEEDED') + expect(res.errors).toHaveLength(1) + expect(res.errors[0].type).toEqual('PIPETTE_VOLUME_EXCEEDED') }) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/dropAllTips.test.js b/protocol-designer/src/step-generation/test-with-flow/dropAllTips.test.js index 3316e38b60d..43c9c0b80d6 100644 --- a/protocol-designer/src/step-generation/test-with-flow/dropAllTips.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/dropAllTips.test.js @@ -3,13 +3,12 @@ import type { RobotState } from '../types' import { makeContext, getInitialRobotStateStandard, - commandCreatorNoErrors, + getSuccessResult, + DEFAULT_PIPETTE, } from './fixtures' -import _dropAllTips from '../commandCreators/atomic/dropAllTips' +import dropAllTips from '../commandCreators/atomic/dropAllTips' -const dropAllTips = commandCreatorNoErrors(_dropAllTips) - -const p300SingleId = 'p300SingleId' +const p300SingleId = DEFAULT_PIPETTE const p300MultiId = 'p300MultiId' let initialRobotState @@ -33,14 +32,16 @@ describe('drop all tips', () => { initialRobotState.tipState.pipettes = {} const result = dropAllTips()(invariantContext, initialRobotState) - expect(result.commands).toHaveLength(0) - expectNoTipsRemaining(result.robotState) + const res = getSuccessResult(result) + expect(res.commands).toHaveLength(0) + expectNoTipsRemaining(res.robotState) }) test('should do nothing with pipette that does not have tips', () => { const result = dropAllTips()(invariantContext, initialRobotState) - expect(result.commands).toHaveLength(0) - expectNoTipsRemaining(result.robotState) + const res = getSuccessResult(result) + expect(res.commands).toHaveLength(0) + expectNoTipsRemaining(res.robotState) }) test('should drop tips of one pipette that has them, and not one without', () => { @@ -50,10 +51,11 @@ describe('drop all tips', () => { } const result = dropAllTips()(invariantContext, initialRobotState) - expect(result.commands).toHaveLength(1) - expect(result.commands[0].params).toMatchObject({ pipette: p300SingleId }) + const res = getSuccessResult(result) + expect(res.commands).toHaveLength(1) + expect(res.commands[0].params).toMatchObject({ pipette: p300SingleId }) - expectNoTipsRemaining(result.robotState) + expectNoTipsRemaining(res.robotState) }) test('should drop tips for both pipettes, with 2 pipettes that have tips', () => { @@ -64,8 +66,9 @@ describe('drop all tips', () => { const result = dropAllTips()(invariantContext, initialRobotState) // order of which pipettes drops tips first is arbitrary - expect(result.commands).toHaveLength(2) + const res = getSuccessResult(result) + expect(res.commands).toHaveLength(2) - expectNoTipsRemaining(result.robotState) + expectNoTipsRemaining(res.robotState) }) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/dropTip.test.js b/protocol-designer/src/step-generation/test-with-flow/dropTip.test.js index eae11b03480..73730487594 100644 --- a/protocol-designer/src/step-generation/test-with-flow/dropTip.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/dropTip.test.js @@ -5,14 +5,14 @@ import { makeStateArgsStandard, makeContext, makeState, - commandCreatorNoErrors, + getSuccessResult, + DEFAULT_PIPETTE, + FIXED_TRASH_ID, } from './fixtures' -import _dropTip from '../commandCreators/atomic/dropTip' +import dropTip from '../commandCreators/atomic/dropTip' import updateLiquidState from '../dispenseUpdateLiquidState' -const dropTip = commandCreatorNoErrors(_dropTip) - jest.mock('../dispenseUpdateLiquidState') describe('dropTip', () => { @@ -48,22 +48,22 @@ describe('dropTip', () => { describe('replaceTip: single channel', () => { test('drop tip if there is a tip', () => { - const result = dropTip('p300SingleId')( + const result = dropTip(DEFAULT_PIPETTE)( invariantContext, makeRobotState({ singleHasTips: true, multiHasTips: true }) ) - - expect(result.commands).toEqual([ + const res = getSuccessResult(result) + expect(res.commands).toEqual([ { - command: 'drop-tip', + command: 'dropTip', params: { - pipette: 'p300SingleId', - labware: 'trashId', + pipette: DEFAULT_PIPETTE, + labware: FIXED_TRASH_ID, well: 'A1', }, }, ]) - expect(result.robotState).toEqual( + expect(res.robotState).toEqual( makeRobotState({ singleHasTips: false, multiHasTips: true }) ) }) @@ -73,12 +73,13 @@ describe('dropTip', () => { singleHasTips: false, multiHasTips: true, }) - const result = dropTip('p300SingleId')( + const result = dropTip(DEFAULT_PIPETTE)( invariantContext, initialRobotState ) - expect(result.commands).toEqual([]) - expect(result.robotState).toEqual(initialRobotState) + const res = getSuccessResult(result) + expect(res.commands).toEqual([]) + expect(res.robotState).toEqual(initialRobotState) }) }) @@ -88,17 +89,18 @@ describe('dropTip', () => { invariantContext, makeRobotState({ singleHasTips: true, multiHasTips: true }) ) - expect(result.commands).toEqual([ + const res = getSuccessResult(result) + expect(res.commands).toEqual([ { - command: 'drop-tip', + command: 'dropTip', params: { pipette: 'p300MultiId', - labware: 'trashId', + labware: FIXED_TRASH_ID, well: 'A1', }, }, ]) - expect(result.robotState).toEqual( + expect(res.robotState).toEqual( makeRobotState({ singleHasTips: true, multiHasTips: false }) ) }) @@ -109,8 +111,9 @@ describe('dropTip', () => { multiHasTips: false, }) const result = dropTip('p300MultiId')(invariantContext, initialRobotState) - expect(result.commands).toEqual([]) - expect(result.robotState).toEqual(initialRobotState) + const res = getSuccessResult(result) + expect(res.commands).toEqual([]) + expect(res.robotState).toEqual(initialRobotState) }) }) @@ -128,19 +131,19 @@ describe('dropTip', () => { }) const result = dropTip('p300MultiId')(invariantContext, initialRobotState) - + const res = getSuccessResult(result) expect(updateLiquidState).toHaveBeenCalledWith( { invariantContext, pipetteId: 'p300MultiId', - labwareId: 'trashId', + labwareId: FIXED_TRASH_ID, useFullVolume: true, well: 'A1', }, robotStateWithTip.liquidState ) - expect(result.robotState.liquidState).toBe(mockLiquidReturnValue) + expect(res.robotState.liquidState).toBe(mockLiquidReturnValue) }) }) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/fixtures/commandFixtures.js b/protocol-designer/src/step-generation/test-with-flow/fixtures/commandFixtures.js index f23ed997271..a8f52afde88 100644 --- a/protocol-designer/src/step-generation/test-with-flow/fixtures/commandFixtures.js +++ b/protocol-designer/src/step-generation/test-with-flow/fixtures/commandFixtures.js @@ -1,93 +1,185 @@ // @flow import { tiprackWellNamesFlat } from './data' import type { - AspirateDispenseArgsV1 as AspirateDispenseArgs, - CommandV1 as Command, -} from '@opentrons/shared-data' + AspirateParams, + BlowoutParams, + DispenseParams, + TouchTipParams, + Command, +} from '@opentrons/shared-data/protocol/flowTypes/schemaV3' +import type { + CommandsAndRobotState, + CommandCreatorErrorResponse, +} from '../../types' + +/** Used to wrap command creators in tests, effectively casting their results + ** to normal response or error response + **/ +export function getSuccessResult( + result: CommandsAndRobotState | CommandCreatorErrorResponse +): CommandsAndRobotState { + if (result.errors) { + throw new Error( + `Expected a successful command creator call but got errors: ${JSON.stringify( + result.errors + )}` + ) + } + return result +} + +export function getErrorResult( + result: CommandsAndRobotState | CommandCreatorErrorResponse +): CommandCreatorErrorResponse { + if (!result.errors) { + throw new Error( + `Expected command creator to return errors but got success result` + ) + } + return result +} export const replaceTipCommands = (tip: number | string): Array => [ - dropTip('A1'), - pickUpTip(tip), + dropTipHelper('A1'), + pickUpTipHelper(tip), ] -export const dropTip = ( +// NOTE: make sure none of these numbers match each other! +const ASPIRATE_FLOW_RATE = 2.1 +const DISPENSE_FLOW_RATE = 2.2 +export const BLOWOUT_FLOW_RATE = 2.3 + +const ASPIRATE_OFFSET_FROM_BOTTOM_MM = 3.1 +const DISPENSE_OFFSET_FROM_BOTTOM_MM = 3.2 +export const BLOWOUT_OFFSET_FROM_TOP_MM = 3.3 +const TOUCH_TIP_OFFSET_FROM_BOTTOM_MM = 3.4 + +export const getFlowRateAndOffsetParams = () => ({ + aspirateFlowRateUlSec: ASPIRATE_FLOW_RATE, + dispenseFlowRateUlSec: DISPENSE_FLOW_RATE, + blowoutFlowRateUlSec: BLOWOUT_FLOW_RATE, + aspirateOffsetFromBottomMm: ASPIRATE_OFFSET_FROM_BOTTOM_MM, + dispenseOffsetFromBottomMm: DISPENSE_OFFSET_FROM_BOTTOM_MM, + blowoutOffsetFromTopMm: BLOWOUT_OFFSET_FROM_TOP_MM, + + // for consolidate/distribute/transfer only + touchTipAfterAspirateOffsetMmFromBottom: TOUCH_TIP_OFFSET_FROM_BOTTOM_MM, + touchTipAfterDispenseOffsetMmFromBottom: TOUCH_TIP_OFFSET_FROM_BOTTOM_MM, + + // for mix only + touchTipMmFromBottom: TOUCH_TIP_OFFSET_FROM_BOTTOM_MM, +}) + +// ================= + +export const DEFAULT_PIPETTE = 'p300SingleId' +export const SOURCE_LABWARE = 'sourcePlateId' +export const DEST_LABWARE = 'destPlateId' +export const TROUGH_LABWARE = 'troughId' +export const FIXED_TRASH_ID = 'trashId' +export const DEFAULT_BLOWOUT_WELL = 'A1' + +// ================= + +const _defaultAspirateParams = { + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, +} +export const makeAspirateHelper = (bakedParams?: $Shape) => ( well: string, - params?: {| pipette?: string, labware?: string |} + volume: number, + params?: $Shape ): Command => ({ - command: 'drop-tip', + command: 'aspirate', params: { - pipette: 'p300SingleId', - labware: 'trashId', - well: typeof well === 'string' ? well : tiprackWellNamesFlat[well], + ..._defaultAspirateParams, + ...bakedParams, + well, + volume, + offsetFromBottomMm: ASPIRATE_OFFSET_FROM_BOTTOM_MM, + flowRate: ASPIRATE_FLOW_RATE, ...params, }, }) -export const pickUpTip = ( - tip: number | string, - params?: {| pipette?: string, labware?: string |} +export const blowoutHelper = ( + labware?: ?string, + params?: $Shape ): Command => ({ - command: 'pick-up-tip', + command: 'blowout', params: { - pipette: 'p300SingleId', - labware: 'tiprack1Id', + pipette: DEFAULT_PIPETTE, + labware: labware || FIXED_TRASH_ID, + well: DEFAULT_BLOWOUT_WELL, + offsetFromBottomMm: BLOWOUT_OFFSET_FROM_TOP_MM, // TODO IMMEDIATELY + flowRate: BLOWOUT_FLOW_RATE, ...params, - well: typeof tip === 'string' ? tip : tiprackWellNamesFlat[tip], }, }) -export const touchTip = ( +const _defaultDispenseParams = { + pipette: DEFAULT_PIPETTE, + labware: DEST_LABWARE, + offsetFromBottomMm: DISPENSE_OFFSET_FROM_BOTTOM_MM, + flowRate: DISPENSE_FLOW_RATE, +} +export const makeDispenseHelper = (bakedParams?: $Shape) => ( well: string, - params?: {| labware?: string |} + volume: number, + params?: $Shape ): Command => ({ - command: 'touch-tip', + command: 'dispense', params: { - labware: 'sourcePlateId', - pipette: 'p300SingleId', - ...params, + ..._defaultDispenseParams, + ...bakedParams, well, + volume, + ...params, }, }) -export const aspirate = ( +const _defaultTouchTipParams = { + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, + offsetFromBottomMm: TOUCH_TIP_OFFSET_FROM_BOTTOM_MM, +} +export const makeTouchTipHelper = (bakedParams?: $Shape) => ( well: string, - volume: number, - params?: $Shape + params?: $Shape ): Command => ({ - command: 'aspirate', + command: 'touchTip', params: { - pipette: 'p300SingleId', - labware: 'sourcePlateId', - ...params, - volume, + ..._defaultTouchTipParams, + ...bakedParams, well, + ...params, }, }) -export const dispense = ( +// ================= + +export const dropTipHelper = ( well: string, - volume: number, - params?: $Shape + params?: {| pipette?: string, labware?: string |} ): Command => ({ - command: 'dispense', + command: 'dropTip', params: { - pipette: 'p300SingleId', - labware: 'sourcePlateId', + pipette: DEFAULT_PIPETTE, + labware: FIXED_TRASH_ID, + well: typeof well === 'string' ? well : tiprackWellNamesFlat[well], ...params, - volume, - well, }, }) -export const blowout = ( - labware?: string, - params?: {| pipette?: string, well?: string |} +export const pickUpTipHelper = ( + tip: number | string, + params?: {| pipette?: string, labware?: string |} ): Command => ({ - command: 'blowout', + command: 'pickUpTip', params: { - pipette: 'p300SingleId', - well: 'A1', - labware: labware || 'trashId', + pipette: DEFAULT_PIPETTE, + labware: 'tiprack1Id', ...params, + well: typeof tip === 'string' ? tip : tiprackWellNamesFlat[tip], }, }) diff --git a/protocol-designer/src/step-generation/test-with-flow/fixtures/index.js b/protocol-designer/src/step-generation/test-with-flow/fixtures/index.js index 07eff0bf5f8..5201fe11943 100644 --- a/protocol-designer/src/step-generation/test-with-flow/fixtures/index.js +++ b/protocol-designer/src/step-generation/test-with-flow/fixtures/index.js @@ -1,5 +1,3 @@ // @flow -import * as commandFixtures from './commandFixtures.js' -export * from './fixtures.js' - -export { commandFixtures } +export * from './commandFixtures.js' +export * from './robotStateFixtures.js' diff --git a/protocol-designer/src/step-generation/test-with-flow/fixtures/fixtures.js b/protocol-designer/src/step-generation/test-with-flow/fixtures/robotStateFixtures.js similarity index 73% rename from protocol-designer/src/step-generation/test-with-flow/fixtures/fixtures.js rename to protocol-designer/src/step-generation/test-with-flow/fixtures/robotStateFixtures.js index 936a6c03fa1..283bc9882fa 100644 --- a/protocol-designer/src/step-generation/test-with-flow/fixtures/fixtures.js +++ b/protocol-designer/src/step-generation/test-with-flow/fixtures/robotStateFixtures.js @@ -13,68 +13,16 @@ import fixture12Trough from '@opentrons/shared-data/labware/fixtures/2/fixture12 import fixtureTipRack10Ul from '@opentrons/shared-data/labware/fixtures/2/fixtureTipRack10Ul.json' import fixtureTipRack300Ul from '@opentrons/shared-data/labware/fixtures/2/fixtureTipRack300Ul.json' -import { makeInitialRobotState, reduceCommandCreators } from '../../utils' +import { + DEFAULT_PIPETTE, + SOURCE_LABWARE, + DEST_LABWARE, + TROUGH_LABWARE, + FIXED_TRASH_ID, +} from './commandFixtures' +import { makeInitialRobotState } from '../../utils' import { tiprackWellNamesFlat } from './data' import type { InvariantContext, RobotState } from '../../' -import type { - CommandsAndRobotState, - CommandCreatorErrorResponse, -} from '../../types' - -/** Used to wrap command creators in tests, effectively casting their results - ** to normal response or error response - **/ -export const commandCreatorNoErrors = (command: any) => (commandArgs: *) => ( - invariantContext: InvariantContext, - robotState: RobotState -): CommandsAndRobotState => { - const result = command(commandArgs)(invariantContext, robotState) - if (result.errors) { - throw new Error('expected no errors, got ' + JSON.stringify(result.errors)) - } - return result -} - -export const commandCreatorHasErrors = (command: *) => (commandArgs: *) => ( - invariantContext: InvariantContext, - robotState: RobotState -): CommandCreatorErrorResponse => { - const result = command(commandArgs)(invariantContext, robotState) - if (!result.errors) { - throw new Error('expected errors') - } - return result -} - -export const compoundCommandCreatorNoErrors = (command: *) => ( - commandArgs: * -) => ( - invariantContext: InvariantContext, - robotState: RobotState -): CommandsAndRobotState => { - const result = reduceCommandCreators( - command(commandArgs)(invariantContext, robotState) - )(invariantContext, robotState) - if (result.errors) { - throw new Error('expected no errors, got ' + JSON.stringify(result.errors)) - } - return result -} - -export const compoundCommandCreatorHasErrors = (command: *) => ( - commandArgs: * -) => ( - invariantContext: InvariantContext, - robotState: RobotState -): CommandCreatorErrorResponse => { - const result = reduceCommandCreators( - command(commandArgs)(invariantContext, robotState) - )(invariantContext, robotState) - if (!result.errors) { - throw new Error('expected errors') - } - return result -} // Eg {A1: true, B1: true, ...} type WellTipState = { [wellName: string]: boolean } @@ -105,23 +53,23 @@ export function getTipColumn( // standard context fixtures to use across tests export function makeContext(): InvariantContext { const labwareEntities = { - trashId: { - id: 'trashId', + [FIXED_TRASH_ID]: { + id: FIXED_TRASH_ID, labwareDefURI: getLabwareDefURI(fixtureTrash), def: fixtureTrash, }, - sourcePlateId: { - id: 'sourcePlateId', + [SOURCE_LABWARE]: { + id: SOURCE_LABWARE, labwareDefURI: getLabwareDefURI(fixture96Plate), def: fixture96Plate, }, - destPlateId: { - id: 'destPlateId', + [DEST_LABWARE]: { + id: DEST_LABWARE, labwareDefURI: getLabwareDefURI(fixture96Plate), def: fixture96Plate, }, - troughId: { - id: 'troughId', + [TROUGH_LABWARE]: { + id: TROUGH_LABWARE, labwareDefURI: getLabwareDefURI(fixture12Trough), def: fixture12Trough, }, @@ -157,9 +105,9 @@ export function makeContext(): InvariantContext { tiprackLabwareDef: fixtureTipRack10Ul, spec: fixtureP10Multi, }, - p300SingleId: { + [DEFAULT_PIPETTE]: { name: 'p300_single', - id: 'p300SingleId', + id: DEFAULT_PIPETTE, tiprackDefURI: getLabwareDefURI(fixtureTipRack300Ul), tiprackLabwareDef: fixtureTipRack300Ul, spec: fixtureP300Single, diff --git a/protocol-designer/src/step-generation/test-with-flow/getNextRobotStateAndWarnings.test.js b/protocol-designer/src/step-generation/test-with-flow/getNextRobotStateAndWarnings.test.js index 2e85fd5454f..12f912e812f 100644 --- a/protocol-designer/src/step-generation/test-with-flow/getNextRobotStateAndWarnings.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/getNextRobotStateAndWarnings.test.js @@ -4,12 +4,12 @@ import forAspirateDispense from '../getNextRobotStateAndWarnings/forAspirateDisp import { makeContext, getRobotStateWithTipStandard, - commandCreatorNoErrors, + getSuccessResult, + DEFAULT_PIPETTE, + SOURCE_LABWARE, } from './fixtures' -import _aspirate from '../commandCreators/atomic/aspirate' - -const aspirate = commandCreatorNoErrors(_aspirate) +import aspirate from '../commandCreators/atomic/aspirate' jest.mock('../getNextRobotStateAndWarnings/forAspirateDispense') @@ -23,13 +23,16 @@ beforeEach(() => { describe('Aspirate Command', () => { test('aspirate from single-ingredient well', () => { const args = { - pipette: 'p300SingleId', - labware: 'sourcePlateId', + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, well: 'A1', volume: 152, + flowRate: 2.22, + offsetFromBottomMm: 1.11, } - const command = aspirate(args)(invariantContext, robotStateWithTip) - .commands[0] + const result = aspirate(args)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + const command = res.commands[0] getNextRobotStateAndWarnings(command, invariantContext, robotStateWithTip) expect(forAspirateDispense).toHaveBeenCalledWith( diff --git a/protocol-designer/src/step-generation/test-with-flow/getNextRobotStateAndWarningsForAspDisp.test.js b/protocol-designer/src/step-generation/test-with-flow/getNextRobotStateAndWarningsForAspDisp.test.js index ea1619de6fa..1405d155475 100644 --- a/protocol-designer/src/step-generation/test-with-flow/getNextRobotStateAndWarningsForAspDisp.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/getNextRobotStateAndWarningsForAspDisp.test.js @@ -1,26 +1,39 @@ // @flow import { AIR, createTipLiquidState } from '../utils' -import { makeContext, getInitialRobotStateStandard } from './fixtures' +import { + makeContext, + getInitialRobotStateStandard, + DEFAULT_PIPETTE, + SOURCE_LABWARE, + TROUGH_LABWARE, +} from './fixtures' import forAspirateDispense from '../getNextRobotStateAndWarnings/forAspirateDispense' import * as warningCreators from '../warningCreators' let invariantContext let initialRobotState +let flowRatesAndOffsets + beforeEach(() => { invariantContext = makeContext() initialRobotState = getInitialRobotStateStandard(invariantContext) + flowRatesAndOffsets = { + flowRate: 1.23, + offsetFromBottomMm: 4.32, + } }) describe('...single-channel pipette', () => { let aspirateSingleCh50FromA1Args - const labwareId = 'troughId' + const labwareId = TROUGH_LABWARE beforeEach(() => { // NOTE: aspirate from TROUGH not sourcePlate aspirateSingleCh50FromA1Args = { + ...flowRatesAndOffsets, labware: labwareId, - pipette: 'p300SingleId', + pipette: DEFAULT_PIPETTE, volume: 50, well: 'A1', } @@ -200,10 +213,11 @@ describe('...single-channel pipette', () => { describe('...8-channel pipette', () => { let aspirate8Ch50FromA1Args - const labwareId = 'sourcePlateId' + const labwareId = SOURCE_LABWARE beforeEach(() => { aspirate8Ch50FromA1Args = { + ...flowRatesAndOffsets, labware: labwareId, pipette: 'p300MultiId', volume: 50, @@ -300,7 +314,7 @@ describe('...8-channel pipette', () => { }) describe('8-channel trough', () => { - const labwareId = 'troughId' + const labwareId = TROUGH_LABWARE const troughCases = [ { testName: '20uLx8 from 300uL trough well', @@ -346,6 +360,7 @@ describe('8-channel trough', () => { } const args = { + ...flowRatesAndOffsets, pipette: 'p300MultiId', well: 'A1', labware: labwareId, diff --git a/protocol-designer/src/step-generation/test-with-flow/mix.test.js b/protocol-designer/src/step-generation/test-with-flow/mix.test.js index db209bada6a..eabf429705a 100644 --- a/protocol-designer/src/step-generation/test-with-flow/mix.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/mix.test.js @@ -1,15 +1,36 @@ // @flow +import flatMap from 'lodash/flatMap' import _mix from '../commandCreators/compound/mix' import { getRobotStateWithTipStandard, makeContext, - compoundCommandCreatorNoErrors, - compoundCommandCreatorHasErrors, - commandFixtures as cmd, + getSuccessResult, + getErrorResult, + replaceTipCommands, + getFlowRateAndOffsetParams, + DEFAULT_PIPETTE, + SOURCE_LABWARE, + DEST_LABWARE, + makeAspirateHelper, + makeDispenseHelper, + blowoutHelper, + makeTouchTipHelper, } from './fixtures' +import { reduceCommandCreators } from '../utils' import type { MixArgs } from '../types' -const mix = compoundCommandCreatorNoErrors(_mix) -const mixWithErrors = compoundCommandCreatorHasErrors(_mix) + +const aspirateHelper = makeAspirateHelper() +const dispenseHelper = makeDispenseHelper({ labware: SOURCE_LABWARE }) +const touchTipHelper = makeTouchTipHelper() +// TODO: Ian 2019-06-14 more elegant way to test the blowout offset calculation +const BLOWOUT_OFFSET_ANY: any = expect.any(Number) + +// collapse this compound command creator into the signature of an atomic command creator +const mix = (args: MixArgs) => (invariantContext, initialRobotState) => + reduceCommandCreators(_mix(args)(invariantContext, initialRobotState))( + invariantContext, + initialRobotState + ) let invariantContext let robotStateWithTip @@ -21,11 +42,12 @@ beforeEach(() => { name: 'mix test', description: 'test blah blah', - pipette: 'p300SingleId', - labware: 'sourcePlateId', + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, blowoutLocation: null, touchTip: false, + ...getFlowRateAndOffsetParams(), } invariantContext = makeContext() @@ -45,122 +67,75 @@ describe('mix: change tip', () => { test('changeTip="always"', () => { const args = makeArgs('always') const result = mix(args)(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ - ...cmd.replaceTipCommands(0), - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - - ...cmd.replaceTipCommands(1), - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - - ...cmd.replaceTipCommands(2), - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - ]) + const res = getSuccessResult(result) + + expect(res.commands).toEqual( + flatMap(args.wells, (well: string, idx: number) => [ + ...replaceTipCommands(idx), + aspirateHelper(well, volume), + dispenseHelper(well, volume), + + aspirateHelper(well, volume), + dispenseHelper(well, volume), + ]) + ) }) test('changeTip="once"', () => { const args = makeArgs('once') const result = mix(args)(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ - ...cmd.replaceTipCommands(0), - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + ...replaceTipCommands(0), + ...flatMap(args.wells, well => [ + aspirateHelper(well, volume), + dispenseHelper(well, volume), + aspirateHelper(well, volume), + dispenseHelper(well, volume), + ]), ]) }) test('changeTip="never"', () => { const args = makeArgs('never') const result = mix(args)(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - ]) + const res = getSuccessResult(result) + + expect(res.commands).toEqual( + flatMap(args.wells, well => [ + aspirateHelper(well, volume), + dispenseHelper(well, volume), + aspirateHelper(well, volume), + dispenseHelper(well, volume), + ]) + ) }) }) describe('mix: advanced options', () => { const volume = 8 const times = 2 - const blowoutLabwareId = 'destPlateId' + const blowoutLabwareId = DEST_LABWARE test('flow rate', () => { - const ASPIRATE_OFFSET = 11 - const DISPENSE_OFFSET = 12 - const ASPIRATE_FLOW_RATE = 3 - const DISPENSE_FLOW_RATE = 6 - const args: MixArgs = { + const args = { ...mixinArgs, volume, times, wells: ['A1'], changeTip: 'once', - aspirateOffsetFromBottomMm: ASPIRATE_OFFSET, - dispenseOffsetFromBottomMm: DISPENSE_OFFSET, - aspirateFlowRateUlSec: ASPIRATE_FLOW_RATE, - dispenseFlowRateUlSec: DISPENSE_FLOW_RATE, - } - - const aspirateParams = { - 'flow-rate': ASPIRATE_FLOW_RATE, - offsetFromBottomMm: ASPIRATE_OFFSET, - } - const dispenseParams = { - 'flow-rate': DISPENSE_FLOW_RATE, - offsetFromBottomMm: DISPENSE_OFFSET, + ...getFlowRateAndOffsetParams(), } const result = mix(args)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - ...cmd.replaceTipCommands(0), - { ...cmd.aspirate('A1', volume, aspirateParams) }, - { ...cmd.dispense('A1', volume, dispenseParams) }, - - { ...cmd.aspirate('A1', volume, aspirateParams) }, - { ...cmd.dispense('A1', volume, dispenseParams) }, + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + ...replaceTipCommands(0), + aspirateHelper('A1', volume), + dispenseHelper('A1', volume), + aspirateHelper('A1', volume), + dispenseHelper('A1', volume), ]) }) @@ -172,39 +147,22 @@ describe('mix: advanced options', () => { changeTip: 'always', touchTip: true, wells: ['A1', 'B1', 'C1'], - aspirateOffsetFromBottomMm: null, - dispenseOffsetFromBottomMm: null, - aspirateFlowRateUlSec: null, - dispenseFlowRateUlSec: null, } const result = mix(args)(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ - ...cmd.replaceTipCommands(0), - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - cmd.touchTip('A1'), - - ...cmd.replaceTipCommands(1), - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - cmd.touchTip('B1'), - - ...cmd.replaceTipCommands(2), - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - cmd.touchTip('C1'), - ]) + const res = getSuccessResult(result) + + expect(res.commands).toEqual( + flatMap(args.wells, (well: string, idx: number) => [ + ...replaceTipCommands(idx), + aspirateHelper(well, volume), + dispenseHelper(well, volume), + + aspirateHelper(well, volume), + dispenseHelper(well, volume), + touchTipHelper(well), + ]) + ) }) test('blowout', () => { @@ -218,32 +176,21 @@ describe('mix: advanced options', () => { } const result = mix(args)(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ - ...cmd.replaceTipCommands(0), - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - cmd.blowout(blowoutLabwareId), - - ...cmd.replaceTipCommands(1), - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - cmd.blowout(blowoutLabwareId), - - ...cmd.replaceTipCommands(2), - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - cmd.blowout(blowoutLabwareId), - ]) + const res = getSuccessResult(result) + + expect(res.commands).toEqual( + flatMap(args.wells, (well, idx) => [ + ...replaceTipCommands(idx), + aspirateHelper(well, volume), + dispenseHelper(well, volume), + + aspirateHelper(well, volume), + dispenseHelper(well, volume), + blowoutHelper(blowoutLabwareId, { + offsetFromBottomMm: BLOWOUT_OFFSET_ANY, + }), + ]) + ) }) test('touch tip after blowout', () => { @@ -258,35 +205,22 @@ describe('mix: advanced options', () => { } const result = mix(args)(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ - ...cmd.replaceTipCommands(0), - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - - cmd.aspirate('A1', volume), - cmd.dispense('A1', volume), - cmd.blowout(blowoutLabwareId), - cmd.touchTip('A1'), - - ...cmd.replaceTipCommands(1), - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - - cmd.aspirate('B1', volume), - cmd.dispense('B1', volume), - cmd.blowout(blowoutLabwareId), - cmd.touchTip('B1'), - - ...cmd.replaceTipCommands(2), - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - - cmd.aspirate('C1', volume), - cmd.dispense('C1', volume), - cmd.blowout(blowoutLabwareId), - cmd.touchTip('C1'), - ]) + const res = getSuccessResult(result) + + expect(res.commands).toEqual( + flatMap(args.wells, (well, idx) => [ + ...replaceTipCommands(idx), + aspirateHelper(well, volume), + dispenseHelper(well, volume), + + aspirateHelper(well, volume), + dispenseHelper(well, volume), + blowoutHelper(blowoutLabwareId, { + offsetFromBottomMm: BLOWOUT_OFFSET_ANY, + }), + touchTipHelper(well), + ]) + ) }) }) @@ -306,9 +240,10 @@ describe('mix: errors', () => { ...errorArgs, labware: 'invalidLabwareId', } - const result = mixWithErrors(args)(invariantContext, robotStateWithTip) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + const result = mix(args)(invariantContext, robotStateWithTip) + const res = getErrorResult(result) + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'LABWARE_DOES_NOT_EXIST', }) }) @@ -318,9 +253,10 @@ describe('mix: errors', () => { ...errorArgs, pipette: 'invalidPipetteId', } - const result = mixWithErrors(args)(invariantContext, robotStateWithTip) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + const result = mix(args)(invariantContext, robotStateWithTip) + const res = getErrorResult(result) + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'PIPETTE_DOES_NOT_EXIST', }) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/replaceTip.test.js b/protocol-designer/src/step-generation/test-with-flow/replaceTip.test.js index 66cbdb8e70a..10e167ee2fa 100644 --- a/protocol-designer/src/step-generation/test-with-flow/replaceTip.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/replaceTip.test.js @@ -5,20 +5,20 @@ import { makeContext, getTiprackTipstate, getTipColumn, - commandCreatorNoErrors, - commandFixtures as cmd, + getSuccessResult, + pickUpTipHelper, + dropTipHelper, + DEFAULT_PIPETTE, } from './fixtures' -import _replaceTip from '../commandCreators/atomic/replaceTip' - +import replaceTip from '../commandCreators/atomic/replaceTip' import updateLiquidState from '../dispenseUpdateLiquidState' -const replaceTip = commandCreatorNoErrors(_replaceTip) - jest.mock('../dispenseUpdateLiquidState') +// TODO: Ian 2019-06-13 move these strings into commandFixtures const tiprack1Id = 'tiprack1Id' const tiprack2Id = 'tiprack2Id' -const p300SingleId = 'p300SingleId' +const p300SingleId = DEFAULT_PIPETTE const p300MultiId = 'p300MultiId' describe('replaceTip', () => { @@ -40,10 +40,11 @@ describe('replaceTip', () => { invariantContext, initialRobotState ) + const res = getSuccessResult(result) - expect(result.commands).toEqual([cmd.pickUpTip(0)]) + expect(res.commands).toEqual([pickUpTipHelper(0)]) - expect(result.robotState).toMatchObject( + expect(res.robotState).toMatchObject( merge({}, initialRobotState, { tipState: { tipracks: { @@ -75,10 +76,11 @@ describe('replaceTip', () => { }, }) ) + const res = getSuccessResult(result) - expect(result.commands).toEqual([cmd.pickUpTip(1)]) + expect(res.commands).toEqual([pickUpTipHelper(1)]) - expect(result.robotState).toMatchObject( + expect(res.robotState).toMatchObject( merge({}, initialRobotState, { tipState: { tipracks: { @@ -111,10 +113,11 @@ describe('replaceTip', () => { invariantContext, initialTestRobotState ) + const res = getSuccessResult(result) - expect(result.commands).toEqual([cmd.pickUpTip('A2')]) + expect(res.commands).toEqual([pickUpTipHelper('A2')]) - expect(result.robotState).toMatchObject( + expect(res.robotState).toMatchObject( merge({}, initialTestRobotState, { tipState: { tipracks: { @@ -148,10 +151,11 @@ describe('replaceTip', () => { invariantContext, initialTestRobotState ) + const res = getSuccessResult(result) - expect(result.commands).toEqual([cmd.dropTip('A1'), cmd.pickUpTip('B1')]) + expect(res.commands).toEqual([dropTipHelper('A1'), pickUpTipHelper('B1')]) - expect(result.robotState).toMatchObject( + expect(res.robotState).toMatchObject( merge({}, initialTestRobotState, { tipState: { tipracks: { @@ -180,12 +184,12 @@ describe('replaceTip', () => { invariantContext, initialTestRobotState ) - - expect(result.commands).toEqual([ - cmd.pickUpTip('A1', { labware: tiprack2Id }), + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + pickUpTipHelper('A1', { labware: tiprack2Id }), ]) - expect(result.robotState).toMatchObject( + expect(res.robotState).toMatchObject( merge({}, initialTestRobotState, { tipState: { tipracks: { @@ -208,12 +212,12 @@ describe('replaceTip', () => { invariantContext, initialRobotState ) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1', { pipette: p300MultiId }), + expect(res.commands).toEqual([ + pickUpTipHelper('A1', { pipette: p300MultiId }), ]) - - expect(result.robotState).toMatchObject( + expect(res.robotState).toMatchObject( merge({}, initialRobotState, { tipState: { tipracks: { @@ -243,12 +247,12 @@ describe('replaceTip', () => { invariantContext, robotStateWithTipA1Missing ) - - expect(result.commands).toEqual([ - cmd.pickUpTip('A2', { pipette: p300MultiId }), + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + pickUpTipHelper('A2', { pipette: p300MultiId }), ]) - expect(result.robotState).toMatchObject( + expect(res.robotState).toMatchObject( merge({}, robotStateWithTipA1Missing, { tipState: { tipracks: { @@ -286,12 +290,13 @@ describe('replaceTip', () => { invariantContext, robotStateWithTipsOnMulti ) - expect(result.commands).toEqual([ - cmd.dropTip('A1', { pipette: p300MultiId }), - cmd.pickUpTip('A1', { pipette: p300MultiId }), + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + dropTipHelper('A1', { pipette: p300MultiId }), + pickUpTipHelper('A1', { pipette: p300MultiId }), ]) - expect(result.robotState).toMatchObject( + expect(res.robotState).toMatchObject( merge({}, robotStateWithTipsOnMulti, { tipState: { tipracks: { diff --git a/protocol-designer/src/step-generation/test-with-flow/robotStateSelectors.test.js b/protocol-designer/src/step-generation/test-with-flow/robotStateSelectors.test.js index b4d6c52cb50..b4be7d5adb6 100644 --- a/protocol-designer/src/step-generation/test-with-flow/robotStateSelectors.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/robotStateSelectors.test.js @@ -7,6 +7,7 @@ import { makeState, getTipColumn, getTiprackTipstate, + DEFAULT_PIPETTE, } from './fixtures' import { sortLabwareBySlot, getNextTiprack, _getNextTip } from '../' @@ -51,7 +52,7 @@ describe('_getNextTip', () => { channel: 1 | 8, tiprackTipState: { [well: string]: boolean } ) => { - const pipetteId = channel === 1 ? 'p300SingleId' : 'p300MultiId' + const pipetteId = channel === 1 ? DEFAULT_PIPETTE : 'p300MultiId' const tiprackId = 'testTiprack' let _invariantContext = makeContext() _invariantContext.labwareEntities[tiprackId] = { @@ -139,7 +140,7 @@ describe('getNextTiprack - single-channel', () => { robotState.tipState.tipracks.tiprack1Id.A1 = false - const result = getNextTiprack('p300SingleId', invariantContext, robotState) + const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) expect(result && result.tiprackId).toEqual('tiprack1Id') expect(result && result.well).toEqual('B1') @@ -153,7 +154,7 @@ describe('getNextTiprack - single-channel', () => { tiprackSetting: { tiprack1Id: false }, }) - const result = getNextTiprack('p300SingleId', invariantContext, robotState) + const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) expect(result).toEqual(null) }) @@ -167,7 +168,7 @@ describe('getNextTiprack - single-channel', () => { }, tiprackSetting: { tiprack1Id: true, tiprack2Id: true }, }) - const result = getNextTiprack('p300SingleId', invariantContext, robotState) + const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) expect(result && result.tiprackId).toEqual('tiprack1Id') expect(result && result.well).toEqual('A1') @@ -186,7 +187,7 @@ describe('getNextTiprack - single-channel', () => { // remove A1 tip from both racks robotState.tipState.tipracks.tiprack1Id.A1 = false robotState.tipState.tipracks.tiprack2Id.A1 = false - const result = getNextTiprack('p300SingleId', invariantContext, robotState) + const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) expect(result && result.tiprackId).toEqual('tiprack1Id') expect(result && result.well).toEqual('B1') @@ -202,7 +203,7 @@ describe('getNextTiprack - single-channel', () => { }, tiprackSetting: { tiprack1Id: false, tiprack2Id: false }, }) - const result = getNextTiprack('p300SingleId', invariantContext, robotState) + const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) expect(result).toBe(null) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/touchTip.test.js b/protocol-designer/src/step-generation/test-with-flow/touchTip.test.js index ab12c21f118..072894449da 100644 --- a/protocol-designer/src/step-generation/test-with-flow/touchTip.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/touchTip.test.js @@ -1,17 +1,16 @@ // @flow import { expectTimelineError } from './testMatchers' -import _touchTip from '../commandCreators/atomic/touchTip' +import touchTip from '../commandCreators/atomic/touchTip' import { getInitialRobotStateStandard, getRobotStateWithTipStandard, makeContext, - commandCreatorNoErrors, - commandCreatorHasErrors, + getSuccessResult, + getErrorResult, + DEFAULT_PIPETTE, + SOURCE_LABWARE, } from './fixtures' -const touchTip = commandCreatorNoErrors(_touchTip) -const touchTipWithErrors = commandCreatorHasErrors(_touchTip) - describe('touchTip', () => { let invariantContext let initialRobotState @@ -23,68 +22,52 @@ describe('touchTip', () => { robotStateWithTip = getRobotStateWithTipStandard(invariantContext) }) - test('touchTip with tip', () => { - const result = touchTip({ - pipette: 'p300SingleId', - labware: 'sourcePlateId', - well: 'A1', - })(invariantContext, robotStateWithTip) - - expect(result.commands).toEqual([ - { - command: 'touch-tip', - params: { - pipette: 'p300SingleId', - labware: 'sourcePlateId', - well: 'A1', - }, - }, - ]) - - expect(result.robotState).toEqual(robotStateWithTip) - }) - test('touchTip with tip, specifying offsetFromBottomMm', () => { const result = touchTip({ - pipette: 'p300SingleId', - labware: 'sourcePlateId', + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, well: 'A1', offsetFromBottomMm: 10, })(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) - expect(result.commands).toEqual([ + expect(res.commands).toEqual([ { - command: 'touch-tip', + command: 'touchTip', params: { - pipette: 'p300SingleId', - labware: 'sourcePlateId', + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, well: 'A1', offsetFromBottomMm: 10, }, }, ]) - expect(result.robotState).toEqual(robotStateWithTip) + expect(res.robotState).toEqual(robotStateWithTip) }) test('touchTip with invalid pipette ID should throw error', () => { - const result = touchTipWithErrors({ + const result = touchTip({ pipette: 'badPipette', - labware: 'sourcePlateId', + labware: SOURCE_LABWARE, well: 'A1', + offsetFromBottomMm: 10, })(invariantContext, robotStateWithTip) + const res = getErrorResult(result) - expectTimelineError(result.errors, 'PIPETTE_DOES_NOT_EXIST') + expectTimelineError(res.errors, 'PIPETTE_DOES_NOT_EXIST') }) test('touchTip with no tip should throw error', () => { - const result = touchTipWithErrors({ - pipette: 'p300SingleId', - labware: 'sourcePlateId', + const result = touchTip({ + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, well: 'A1', + offsetFromBottomMm: 10, })(invariantContext, initialRobotState) + const res = getErrorResult(result) - expect(result.errors).toEqual([ + expect(res.errors).toEqual([ { message: "Attempted to touchTip with no tip on pipette: p300SingleId from sourcePlateId's well A1", diff --git a/protocol-designer/src/step-generation/test-with-flow/transfer.test.js b/protocol-designer/src/step-generation/test-with-flow/transfer.test.js index 62d9ef5e4ce..4278542ea7c 100644 --- a/protocol-designer/src/step-generation/test-with-flow/transfer.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/transfer.test.js @@ -3,28 +3,50 @@ import merge from 'lodash/merge' import { getRobotStateWithTipStandard, makeContext, - compoundCommandCreatorNoErrors, - compoundCommandCreatorHasErrors, - commandFixtures as cmd, + getSuccessResult, + getErrorResult, + getFlowRateAndOffsetParams, + DEFAULT_PIPETTE, + SOURCE_LABWARE, + DEST_LABWARE, + makeAspirateHelper, + makeDispenseHelper, + makeTouchTipHelper, + pickUpTipHelper, + dropTipHelper, } from './fixtures' +import { reduceCommandCreators } from '../utils' import _transfer from '../commandCreators/compound/transfer' +import type { TransferArgs } from '../types' + +const aspirateHelper = makeAspirateHelper() +const dispenseHelper = makeDispenseHelper() +const touchTipHelper = makeTouchTipHelper() + +// collapse this compound command creator into the signature of an atomic command creator +const transfer = (args: TransferArgs) => ( + invariantContext, + initialRobotState +) => + reduceCommandCreators(_transfer(args)(invariantContext, initialRobotState))( + invariantContext, + initialRobotState + ) -const transfer = compoundCommandCreatorNoErrors(_transfer) -const transferWithErrors = compoundCommandCreatorHasErrors(_transfer) - -let transferArgs let invariantContext let robotStateWithTip +let mixinArgs beforeEach(() => { - transferArgs = { + mixinArgs = { + ...getFlowRateAndOffsetParams(), commandCreatorFnName: 'transfer', name: 'Transfer Test', description: 'test blah blah', - pipette: 'p300SingleId', + pipette: DEFAULT_PIPETTE, - sourceLabware: 'sourcePlateId', - destLabware: 'destPlateId', + sourceLabware: SOURCE_LABWARE, + destLabware: DEST_LABWARE, preWetTip: false, touchTipAfterAspirate: false, @@ -41,8 +63,8 @@ beforeEach(() => { describe('pick up tip if no tip on pipette', () => { beforeEach(() => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, sourceWells: ['A1'], destWells: ['B2'], volume: 30, @@ -56,38 +78,37 @@ describe('pick up tip if no tip on pipette', () => { changeTipOptions.forEach(changeTip => { test(`...${changeTip}`, () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, changeTip, } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) - expect(result.commands[0]).toEqual(cmd.pickUpTip('A1')) + expect(res.commands[0]).toEqual(pickUpTipHelper('A1')) }) }) test('...never (should not pick up tip, and fail)', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, changeTip: 'never', } - const result = transferWithErrors(transferArgs)( - invariantContext, - robotStateWithTip - ) + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getErrorResult(result) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'NO_TIP_ON_PIPETTE', }) }) }) test('single transfer: 1 source & 1 dest', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, sourceWells: ['A1'], destWells: ['B2'], changeTip: 'never', @@ -98,13 +119,14 @@ test('single transfer: 1 source & 1 dest', () => { '0': { volume: 200 }, } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.aspirate('A1', 30), - cmd.dispense('B2', 30, { labware: 'destPlateId' }), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + aspirateHelper('A1', 30), + dispenseHelper('B2', 30), ]) - expect(result.robotState.liquidState).toEqual( + expect(res.robotState.liquidState).toEqual( merge({}, robotStateWithTip.liquidState, { labware: { sourcePlateId: { A1: { '0': { volume: 200 - 30 } } }, @@ -118,28 +140,29 @@ test('single transfer: 1 source & 1 dest', () => { }) test('transfer with multiple sets of wells', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, sourceWells: ['A1', 'A2'], destWells: ['B2', 'C2'], changeTip: 'never', volume: 30, } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.aspirate('A1', 30), - cmd.dispense('B2', 30, { labware: 'destPlateId' }), - - cmd.aspirate('A2', 30), - cmd.dispense('C2', 30, { labware: 'destPlateId' }), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + aspirateHelper('A1', 30), + dispenseHelper('B2', 30), + + aspirateHelper('A2', 30), + dispenseHelper('C2', 30), ]) // TODO Ian 2018-04-02 robotState, liquidState checks }) test('invalid pipette ID should throw error', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, sourceWells: ['A1'], destWells: ['B1'], volume: 10, @@ -147,20 +170,18 @@ test('invalid pipette ID should throw error', () => { pipette: 'no-such-pipette-id-here', } - const result = transferWithErrors(transferArgs)( - invariantContext, - robotStateWithTip - ) + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getErrorResult(result) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'PIPETTE_DOES_NOT_EXIST', }) }) test('invalid labware ID should throw error', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, sourceLabware: 'no-such-labware-id-here', sourceWells: ['A1'], destWells: ['B1'], @@ -168,13 +189,11 @@ test('invalid labware ID should throw error', () => { changeTip: 'always', } - const result = transferWithErrors(transferArgs)( - invariantContext, - robotStateWithTip - ) + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getErrorResult(result) - expect(result.errors).toHaveLength(1) - expect(result.errors[0]).toMatchObject({ + expect(res.errors).toHaveLength(1) + expect(res.errors[0]).toMatchObject({ type: 'LABWARE_DOES_NOT_EXIST', }) }) @@ -182,8 +201,8 @@ test('invalid labware ID should throw error', () => { describe('single transfer exceeding pipette max', () => { let expectedFinalLiquidState beforeEach(() => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, sourceWells: ['A1', 'B1'], destWells: ['A3', 'B3'], volume: 350, @@ -223,62 +242,64 @@ describe('single transfer exceeding pipette max', () => { }) test('changeTip="once"', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, changeTip: 'once', } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), - cmd.aspirate('A1', 300), - cmd.dispense('A3', 300, { labware: 'destPlateId' }), - cmd.aspirate('A1', 50), - cmd.dispense('A3', 50, { labware: 'destPlateId' }), - cmd.aspirate('B1', 300), - cmd.dispense('B3', 300, { labware: 'destPlateId' }), - cmd.aspirate('B1', 50), - cmd.dispense('B3', 50, { labware: 'destPlateId' }), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 300), + dispenseHelper('A3', 300), + aspirateHelper('A1', 50), + dispenseHelper('A3', 50), + aspirateHelper('B1', 300), + dispenseHelper('B3', 300), + aspirateHelper('B1', 50), + dispenseHelper('B3', 50), ]) - expect(result.robotState.liquidState).toEqual( + expect(res.robotState.liquidState).toEqual( merge({}, robotStateWithTip.liquidState, expectedFinalLiquidState) ) }) test('changeTip="always"', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, changeTip: 'always', } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), - cmd.aspirate('A1', 300), - cmd.dispense('A3', 300, { labware: 'destPlateId' }), + aspirateHelper('A1', 300), + dispenseHelper('A3', 300), // replace tip before next asp-disp chunk - cmd.dropTip('A1'), - cmd.pickUpTip('B1'), + dropTipHelper('A1'), + pickUpTipHelper('B1'), - cmd.aspirate('A1', 50), - cmd.dispense('A3', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('A3', 50), // replace tip before next source-dest well pair - cmd.dropTip('A1'), - cmd.pickUpTip('C1'), + dropTipHelper('A1'), + pickUpTipHelper('C1'), - cmd.aspirate('B1', 300), - cmd.dispense('B3', 300, { labware: 'destPlateId' }), + aspirateHelper('B1', 300), + dispenseHelper('B3', 300), // replace tip before next asp-disp chunk - cmd.dropTip('A1'), - cmd.pickUpTip('D1'), + dropTipHelper('A1'), + pickUpTipHelper('D1'), - cmd.aspirate('B1', 50), - cmd.dispense('B3', 50, { labware: 'destPlateId' }), + aspirateHelper('B1', 50), + dispenseHelper('B3', 50), ]) // unlike the other test cases here, we have a new tip when aspirating from B1. @@ -290,154 +311,155 @@ describe('single transfer exceeding pipette max', () => { // $FlowFixMe flow doesn't like assigning to these objects expectedFinalLiquidState.labware.destPlateId.B3 = { '1': { volume: 350 } } - expect(result.robotState.liquidState).toEqual( + expect(res.robotState.liquidState).toEqual( merge({}, robotStateWithTip.liquidState, expectedFinalLiquidState) ) }) test('changeTip="perSource"', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, sourceWells: ['A1', 'A1', 'A2'], destWells: ['B1', 'B2', 'B2'], changeTip: 'perSource', } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), - cmd.aspirate('A1', 300), - cmd.dispense('B1', 300, { labware: 'destPlateId' }), + aspirateHelper('A1', 300), + dispenseHelper('B1', 300), - cmd.aspirate('A1', 50), - cmd.dispense('B1', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('B1', 50), // same source, different dest: no change - cmd.aspirate('A1', 300), - cmd.dispense('B2', 300, { labware: 'destPlateId' }), + aspirateHelper('A1', 300), + dispenseHelper('B2', 300), - cmd.aspirate('A1', 50), - cmd.dispense('B2', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('B2', 50), // new source, different dest: change tip - cmd.dropTip('A1'), - cmd.pickUpTip('B1'), + dropTipHelper('A1'), + pickUpTipHelper('B1'), - cmd.aspirate('A2', 300), - cmd.dispense('B2', 300, { labware: 'destPlateId' }), + aspirateHelper('A2', 300), + dispenseHelper('B2', 300), - cmd.aspirate('A2', 50), - cmd.dispense('B2', 50, { labware: 'destPlateId' }), + aspirateHelper('A2', 50), + dispenseHelper('B2', 50), ]) }) test('changeTip="perDest"', () => { // NOTE: same wells as perSource test - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, sourceWells: ['A1', 'A1', 'A2'], destWells: ['B1', 'B2', 'B2'], changeTip: 'perDest', } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.pickUpTip('A1'), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), - cmd.aspirate('A1', 300), - cmd.dispense('B1', 300, { labware: 'destPlateId' }), + aspirateHelper('A1', 300), + dispenseHelper('B1', 300), - cmd.aspirate('A1', 50), - cmd.dispense('B1', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('B1', 50), // same source, different dest: change tip - cmd.dropTip('A1'), - cmd.pickUpTip('B1'), + dropTipHelper('A1'), + pickUpTipHelper('B1'), - cmd.aspirate('A1', 300), - cmd.dispense('B2', 300, { labware: 'destPlateId' }), + aspirateHelper('A1', 300), + dispenseHelper('B2', 300), - cmd.aspirate('A1', 50), - cmd.dispense('B2', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('B2', 50), // different source, same dest: no change - cmd.aspirate('A2', 300), - cmd.dispense('B2', 300, { labware: 'destPlateId' }), + aspirateHelper('A2', 300), + dispenseHelper('B2', 300), - cmd.aspirate('A2', 50), - cmd.dispense('B2', 50, { labware: 'destPlateId' }), + aspirateHelper('A2', 50), + dispenseHelper('B2', 50), ]) }) test('changeTip="never"', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, changeTip: 'never', } // begin with tip on pipette robotStateWithTip.tipState.pipettes.p300SingleId = true - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ // no pick up tip - cmd.aspirate('A1', 300), - cmd.dispense('A3', 300, { labware: 'destPlateId' }), + aspirateHelper('A1', 300), + dispenseHelper('A3', 300), - cmd.aspirate('A1', 50), - cmd.dispense('A3', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('A3', 50), - cmd.aspirate('B1', 300), - cmd.dispense('B3', 300, { labware: 'destPlateId' }), + aspirateHelper('B1', 300), + dispenseHelper('B3', 300), - cmd.aspirate('B1', 50), - cmd.dispense('B3', 50, { labware: 'destPlateId' }), + aspirateHelper('B1', 50), + dispenseHelper('B3', 50), ]) - expect(result.robotState.liquidState).toEqual( + expect(res.robotState.liquidState).toEqual( merge({}, robotStateWithTip.liquidState, expectedFinalLiquidState) ) }) test('split up volume without going below pipette min', () => { - // TODO: Ian 2019-01-04 for some reason, doing transferArgs = {...transferArgs, ...etc} - // works everywhere but here - here, it makes Jest fail with "Jest encountered an unexpected token" - const _transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, volume: 629, changeTip: 'never', // don't test tip use here } - transferArgs = _transferArgs // begin with tip on pipette robotStateWithTip.tipState.pipettes.p300SingleId = true - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.aspirate('A1', 300), - cmd.dispense('A3', 300, { labware: 'destPlateId' }), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + aspirateHelper('A1', 300), + dispenseHelper('A3', 300), // last 2 chunks split evenly - cmd.aspirate('A1', 164.5), - cmd.dispense('A3', 164.5, { labware: 'destPlateId' }), - cmd.aspirate('A1', 164.5), - cmd.dispense('A3', 164.5, { labware: 'destPlateId' }), + aspirateHelper('A1', 164.5), + dispenseHelper('A3', 164.5), + aspirateHelper('A1', 164.5), + dispenseHelper('A3', 164.5), - cmd.aspirate('B1', 300), - cmd.dispense('B3', 300, { labware: 'destPlateId' }), + aspirateHelper('B1', 300), + dispenseHelper('B3', 300), // last 2 chunks split evenly - cmd.aspirate('B1', 164.5), - cmd.dispense('B3', 164.5, { labware: 'destPlateId' }), - cmd.aspirate('B1', 164.5), - cmd.dispense('B3', 164.5, { labware: 'destPlateId' }), + aspirateHelper('B1', 164.5), + dispenseHelper('B3', 164.5), + aspirateHelper('B1', 164.5), + dispenseHelper('B3', 164.5), ]) }) }) describe('advanced options', () => { beforeEach(() => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, sourceWells: ['A1'], destWells: ['B1'], changeTip: 'never', @@ -445,68 +467,71 @@ describe('advanced options', () => { }) describe('...aspirate options', () => { test('pre-wet tip should aspirate and dispense transfer volume from source well of each subtransfer', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, volume: 350, preWetTip: true, } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ // pre-wet aspirate/dispense - cmd.aspirate('A1', 300), - cmd.dispense('A1', 300), + aspirateHelper('A1', 300), + dispenseHelper('A1', 300, { labware: SOURCE_LABWARE }), // "real" aspirate/dispenses - cmd.aspirate('A1', 300), - cmd.dispense('B1', 300, { labware: 'destPlateId' }), + aspirateHelper('A1', 300), + dispenseHelper('B1', 300), - cmd.aspirate('A1', 50), - cmd.dispense('B1', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('B1', 50), ]) }) - test('touch-tip after aspirate should touch-tip on each source well, for every aspirate', () => { - transferArgs = { - ...transferArgs, + test('touchTip after aspirate should touchTip on each source well, for every aspirate', () => { + mixinArgs = { + ...mixinArgs, volume: 350, touchTipAfterAspirate: true, } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.aspirate('A1', 300), - cmd.touchTip('A1'), - cmd.dispense('B1', 300, { labware: 'destPlateId' }), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + aspirateHelper('A1', 300), + touchTipHelper('A1'), + dispenseHelper('B1', 300), - cmd.aspirate('A1', 50), - cmd.touchTip('A1'), - cmd.dispense('B1', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + touchTipHelper('A1'), + dispenseHelper('B1', 50), ]) }) - test('touch-tip after dispense should touch-tip on each dest well, for every dispense', () => { - transferArgs = { - ...transferArgs, + test('touchTip after dispense should touchTip on each dest well, for every dispense', () => { + mixinArgs = { + ...mixinArgs, volume: 350, touchTipAfterDispense: true, } - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.aspirate('A1', 300), - cmd.dispense('B1', 300, { labware: 'destPlateId' }), - cmd.touchTip('B1', { labware: 'destPlateId' }), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + aspirateHelper('A1', 300), + dispenseHelper('B1', 300), + touchTipHelper('B1', { labware: DEST_LABWARE }), - cmd.aspirate('A1', 50), - cmd.dispense('B1', 50, { labware: 'destPlateId' }), - cmd.touchTip('B1', { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('B1', 50), + touchTipHelper('B1', { labware: DEST_LABWARE }), ]) }) test('mix before aspirate', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, volume: 350, mixBeforeAspirate: { volume: 250, @@ -517,22 +542,23 @@ describe('advanced options', () => { // written here for less verbose `commands` below const mixCommands = [ // mix 1 - cmd.aspirate('A1', 250), - cmd.dispense('A1', 250), + aspirateHelper('A1', 250), + dispenseHelper('A1', 250, { labware: SOURCE_LABWARE }), // mix 2 - cmd.aspirate('A1', 250), - cmd.dispense('A1', 250), + aspirateHelper('A1', 250), + dispenseHelper('A1', 250, { labware: SOURCE_LABWARE }), ] - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ ...mixCommands, - cmd.aspirate('A1', 300), - cmd.dispense('B1', 300, { labware: 'destPlateId' }), + aspirateHelper('A1', 300), + dispenseHelper('B1', 300), ...mixCommands, - cmd.aspirate('A1', 50), - cmd.dispense('B1', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('B1', 50), ]) }) @@ -541,8 +567,8 @@ describe('advanced options', () => { describe('...dispense options', () => { test('mix after dispense', () => { - transferArgs = { - ...transferArgs, + mixinArgs = { + ...mixinArgs, volume: 350, mixInDestination: { volume: 250, @@ -553,25 +579,24 @@ describe('advanced options', () => { // written here for less verbose `commands` below const mixCommands = [ // mix 1 - cmd.aspirate('B1', 250, { labware: 'destPlateId' }), - cmd.dispense('B1', 250, { labware: 'destPlateId' }), + aspirateHelper('B1', 250, { labware: DEST_LABWARE }), + dispenseHelper('B1', 250), // mix 2 - cmd.aspirate('B1', 250, { labware: 'destPlateId' }), - cmd.dispense('B1', 250, { labware: 'destPlateId' }), + aspirateHelper('B1', 250, { labware: DEST_LABWARE }), + dispenseHelper('B1', 250), ] - const result = transfer(transferArgs)(invariantContext, robotStateWithTip) - expect(result.commands).toEqual([ - cmd.aspirate('A1', 300), - cmd.dispense('B1', 300, { labware: 'destPlateId' }), + const result = transfer(mixinArgs)(invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + aspirateHelper('A1', 300), + dispenseHelper('B1', 300), ...mixCommands, - cmd.aspirate('A1', 50), - cmd.dispense('B1', 50, { labware: 'destPlateId' }), + aspirateHelper('A1', 50), + dispenseHelper('B1', 50), ...mixCommands, ]) }) - - test.skip('blowout should blowout in specified labware after each dispense', () => {}) // TODO }) }) diff --git a/protocol-designer/src/step-generation/test-with-flow/utils.test.js b/protocol-designer/src/step-generation/test-with-flow/utils.test.js index a3b576ff348..ab09f0a1713 100644 --- a/protocol-designer/src/step-generation/test-with-flow/utils.test.js +++ b/protocol-designer/src/step-generation/test-with-flow/utils.test.js @@ -19,6 +19,7 @@ import { repeatArray, makeInitialRobotState, } from '../utils' +import { FIXED_TRASH_ID } from './fixtures' import type { InvariantContext } from '../types' let invariantContext @@ -440,7 +441,7 @@ describe('makeInitialRobotState', () => { def: fixtureTipRack300Ul, }, trashId: { - id: 'trashId', + id: FIXED_TRASH_ID, labwareDefURI: getLabwareDefURI(fixtureTrash), def: fixtureTrash, }, diff --git a/protocol-designer/src/step-generation/types.js b/protocol-designer/src/step-generation/types.js index 7f96daad5be..7ee3094ca22 100644 --- a/protocol-designer/src/step-generation/types.js +++ b/protocol-designer/src/step-generation/types.js @@ -1,8 +1,5 @@ // @flow -import type { - CommandV1 as Command, - PipetteLabwareFieldsV1 as PipetteLabwareFields, -} from '@opentrons/shared-data' +import type { Command } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { LabwareTemporalProperties, PipetteTemporalProperties, @@ -49,23 +46,23 @@ export type SharedTransferLikeArgs = { /** Touch tip after every aspirate */ touchTipAfterAspirate: boolean, /** Optional offset for touch tip after aspirate (if null, use PD default) */ - touchTipAfterAspirateOffsetMmFromBottom?: ?number, + touchTipAfterAspirateOffsetMmFromBottom: number, /** changeTip is interpreted differently by different Step types */ changeTip: ChangeTipOptions, /** Flow rate in uL/sec for all aspirates */ - aspirateFlowRateUlSec?: ?number, + aspirateFlowRateUlSec: number, /** offset from bottom of well in mm */ - aspirateOffsetFromBottomMm?: ?number, + aspirateOffsetFromBottomMm: number, // ===== DISPENSE SETTINGS ===== /** Touch tip in destination well after dispense */ touchTipAfterDispense: boolean, /** Optional offset for touch tip after dispense (if null, use PD default) */ - touchTipAfterDispenseOffsetMmFromBottom?: ?number, + touchTipAfterDispenseOffsetMmFromBottom: number, /** Flow rate in uL/sec for all dispenses */ - dispenseFlowRateUlSec?: ?number, + dispenseFlowRateUlSec: number, /** offset from bottom of well in mm */ - dispenseOffsetFromBottomMm?: ?number, + dispenseOffsetFromBottomMm: number, } export type ConsolidateArgs = { @@ -76,6 +73,8 @@ export type ConsolidateArgs = { /** If given, blow out in the specified destination after dispense at the end of each asp-asp-dispense cycle */ blowoutLocation: ?string, + blowoutFlowRateUlSec: number, + blowoutOffsetFromTopMm: number, /** Mix in first well in chunk */ mixFirstAspirate: ?InnerMixArgs, @@ -91,6 +90,8 @@ export type TransferArgs = { /** If given, blow out in the specified destination after dispense at the end of each asp-dispense cycle */ blowoutLocation: ?string, + blowoutFlowRateUlSec: number, + blowoutOffsetFromTopMm: number, /** Mix in first well in chunk */ mixBeforeAspirate: ?InnerMixArgs, @@ -110,6 +111,10 @@ export type DistributeArgs = { disposalLabware: ?string, disposalWell: ?string, + /** pass to blowout **/ + blowoutFlowRateUlSec: number, + blowoutOffsetFromTopMm: number, + /** Mix in first well in chunk */ mixBeforeAspirate: ?InnerMixArgs, } & SharedTransferLikeArgs @@ -126,22 +131,24 @@ export type MixArgs = { times: number, /** Touch tip after mixing */ touchTip: boolean, - touchTipMmFromBottom?: ?number, + touchTipMmFromBottom: number, /** change tip: see comments in step-generation/mix.js */ changeTip: ChangeTipOptions, /** If given, blow out in the specified destination after mixing each well */ blowoutLocation: ?string, + blowoutFlowRateUlSec: number, + blowoutOffsetFromTopMm: number, /** offset from bottom of well in mm */ - aspirateOffsetFromBottomMm?: ?number, - dispenseOffsetFromBottomMm?: ?number, + aspirateOffsetFromBottomMm: number, + dispenseOffsetFromBottomMm: number, /** flow rates in uL/sec */ - aspirateFlowRateUlSec?: ?number, - dispenseFlowRateUlSec?: ?number, + aspirateFlowRateUlSec: number, + dispenseFlowRateUlSec: number, } -export type DelayArgs = { +export type PauseArgs = { ...$Exact, commandCreatorFnName: 'delay', message?: string, @@ -157,7 +164,7 @@ export type CommandCreatorArgs = | ConsolidateArgs | DistributeArgs | MixArgs - | DelayArgs + | PauseArgs | TransferArgs /** tips are numbered 0-7. 0 is the furthest to the back of the robot. @@ -216,11 +223,6 @@ export type RobotState = {| }, |} -export type TouchTipArgs = {| - ...PipetteLabwareFields, - offsetFromBottomMm?: ?number, -|} - export type ErrorType = | 'INSUFFICIENT_TIPS' | 'MISMATCHED_SOURCE_DEST_WELLS' diff --git a/protocol-designer/src/step-generation/utils.js b/protocol-designer/src/step-generation/utils.js index cec0f602d9c..d618f6f2cb8 100644 --- a/protocol-designer/src/step-generation/utils.js +++ b/protocol-designer/src/step-generation/utils.js @@ -1,4 +1,5 @@ // @flow +import assert from 'assert' import cloneDeep from 'lodash/cloneDeep' import flatMap from 'lodash/flatMap' import mapValues from 'lodash/mapValues' @@ -6,15 +7,14 @@ import range from 'lodash/range' import reduce from 'lodash/reduce' import last from 'lodash/last' import { - getWellNamePerMultiTip, getIsTiprack, getLabwareDefURI, + getWellsDepth, + getWellNamePerMultiTip, } from '@opentrons/shared-data' -import type { - PipetteLabwareFieldsV1 as PipetteLabwareFields, - LabwareDefinition2, -} from '@opentrons/shared-data' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { BlowoutParams } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { PipetteEntity, LabwareEntity } from '../step-forms' import type { CommandCreator, @@ -258,27 +258,65 @@ export function totalVolume(location: LocationLiquidState): number { }, 0) } -export const blowoutUtil = ( - pipette: $PropertyType, - sourceLabware: $PropertyType, - sourceWell: $PropertyType, - destLabware: $PropertyType, - destWell: $PropertyType, - blowoutLocation: ?string -): Array => { +// Set blowout location depending on the 'blowoutLocation' arg: set it to +// the SOURCE_WELL_BLOWOUT_DESTINATION / DEST_WELL_BLOWOUT_DESTINATION +// special strings, or to a labware ID. +export const blowoutUtil = (args: { + pipette: $PropertyType, + sourceLabwareId: string, + sourceWell: $PropertyType, + destLabwareId: string, + destWell: $PropertyType, + blowoutLocation: ?string, + flowRate: number, + offsetFromTopMm: number, + invariantContext: InvariantContext, +}): Array => { + const { + pipette, + sourceLabwareId, + sourceWell, + destLabwareId, + destWell, + blowoutLocation, + flowRate, + offsetFromTopMm, + invariantContext, + } = args + if (!blowoutLocation) return [] - let labware = blowoutLocation - let well = 'A1' + let labware + let well - // TODO Ian 2018-05-04 more explicit test for non-trash blowout destination if (blowoutLocation === SOURCE_WELL_BLOWOUT_DESTINATION) { - labware = sourceLabware + labware = invariantContext.labwareEntities[sourceLabwareId] well = sourceWell } else if (blowoutLocation === DEST_WELL_BLOWOUT_DESTINATION) { - labware = destLabware + labware = invariantContext.labwareEntities[destLabwareId] well = destWell + } else { + // if it's not one of the magic strings, it's a labware id + labware = invariantContext.labwareEntities?.[blowoutLocation] + well = 'A1' + if (!labware) { + assert( + false, + `expected a labwareId for blowoutUtil's "blowoutLocation", got ${blowoutLocation}` + ) + return [] + } } - return [blowout({ pipette: pipette, labware, well })] + const offsetFromBottomMm = + getWellsDepth(labware.def, [well]) + offsetFromTopMm + return [ + blowout({ + pipette: pipette, + labware: labware.id, + well, + flowRate, + offsetFromBottomMm, + }), + ] } export function createEmptyLiquidState(invariantContext: InvariantContext) { diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.js index d24eb81f178..d79934df008 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.js @@ -1,16 +1,24 @@ // @flow import assert from 'assert' -import { DEFAULT_CHANGE_TIP_OPTION } from '../../../constants' +import { getWellsDepth } from '@opentrons/shared-data' +import { + DEFAULT_CHANGE_TIP_OPTION, + DEFAULT_MM_FROM_BOTTOM_ASPIRATE, + DEFAULT_MM_FROM_BOTTOM_DISPENSE, + DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, +} from '../../../constants' import { getOrderedWells } from '../../utils' -import type { FormData } from '../../../form-types' +import type { HydratedMixFormDataLegacy } from '../../../form-types' import type { MixArgs } from '../../../step-generation' type MixStepArgs = MixArgs -const mixFormToArgs = (hydratedFormData: FormData): MixStepArgs => { +const mixFormToArgs = ( + hydratedFormData: HydratedMixFormDataLegacy +): MixStepArgs => { + console.log('mixFormToArgs', { hydratedFormData }) const { labware, pipette } = hydratedFormData - const touchTip = Boolean(hydratedFormData['mix_touchTip_checkbox']) - const touchTipMmFromBottom = hydratedFormData['mix_touchTip_mmFromBottom'] let unorderedWells = hydratedFormData.wells || [] const orderFirst = hydratedFormData.mix_wellOrder_first @@ -23,16 +31,28 @@ const mixFormToArgs = (hydratedFormData: FormData): MixStepArgs => { orderSecond ) - const volume = hydratedFormData.volume - const times = hydratedFormData.times + const touchTip = Boolean(hydratedFormData['mix_touchTip_checkbox']) + const touchTipMmFromBottom = + hydratedFormData['mix_touchTip_mmFromBottom'] || + getWellsDepth(labware.def, orderedWells) + + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP + + const volume = hydratedFormData.volume || 0 + const times = hydratedFormData.times || 0 - const aspirateFlowRateUlSec = hydratedFormData['aspirate_flowRate'] - const dispenseFlowRateUlSec = hydratedFormData['dispense_flowRate'] + const aspirateFlowRateUlSec = + hydratedFormData['aspirate_flowRate'] || + pipette.spec.defaultAspirateFlowRate.value + const dispenseFlowRateUlSec = + hydratedFormData['dispense_flowRate'] || + pipette.spec.defaultDispenseFlowRate.value // NOTE: for mix, there is only one tip offset field, // and it applies to both aspirate and dispense - const aspirateOffsetFromBottomMm = hydratedFormData['mix_mmFromBottom'] - const dispenseOffsetFromBottomMm = hydratedFormData['mix_mmFromBottom'] + const aspirateOffsetFromBottomMm = + hydratedFormData['mix_mmFromBottom'] || DEFAULT_MM_FROM_BOTTOM_ASPIRATE + const dispenseOffsetFromBottomMm = + hydratedFormData['mix_mmFromBottom'] || DEFAULT_MM_FROM_BOTTOM_DISPENSE // It's radiobutton, so one should always be selected. // One changeTip option should always be selected. @@ -46,6 +66,12 @@ const mixFormToArgs = (hydratedFormData: FormData): MixStepArgs => { ? hydratedFormData['blowout_location'] : null + // Blowout settings + const blowoutFlowRateUlSec = dispenseFlowRateUlSec + const blowoutOffsetFromTopMm = blowoutLocation + ? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + : 0 + return { commandCreatorFnName: 'mix', name: `Mix ${hydratedFormData.id}`, // TODO real name for steps @@ -61,8 +87,10 @@ const mixFormToArgs = (hydratedFormData: FormData): MixStepArgs => { pipette: pipette.id, aspirateFlowRateUlSec, dispenseFlowRateUlSec, + blowoutFlowRateUlSec, aspirateOffsetFromBottomMm, dispenseOffsetFromBottomMm, + blowoutOffsetFromTopMm, } } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.js index 215f04531cc..39c3275acb3 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.js @@ -1,9 +1,16 @@ // @flow import assert from 'assert' +import { getWellsDepth } from '@opentrons/shared-data' import { DEST_WELL_BLOWOUT_DESTINATION, SOURCE_WELL_BLOWOUT_DESTINATION, } from '../../../step-generation/utils' +import { + DEFAULT_MM_FROM_BOTTOM_ASPIRATE, + DEFAULT_MM_FROM_BOTTOM_DISPENSE, + DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, +} from '../../../constants' import { getOrderedWells } from '../../utils' import type { HydratedMoveLiquidFormData } from '../../../form-types' @@ -47,6 +54,7 @@ const moveLiquidFormToArgs = ( ) const fields = hydratedFormData.fields + const pipetteSpec = fields.pipette.spec const pipetteId = fields.pipette.id const { @@ -58,15 +66,67 @@ const moveLiquidFormToArgs = ( path, } = fields + let sourceWells = getOrderedWells( + fields.aspirate_wells, + sourceLabware.def, + fields.aspirate_wellOrder_first, + fields.aspirate_wellOrder_second + ) + + let destWells = getOrderedWells( + fields.dispense_wells, + destLabware.def, + fields.dispense_wellOrder_first, + fields.dispense_wellOrder_second + ) + + // 1:many with single path: spread well array of length 1 to match other well array + if (path === 'single' && sourceWells.length !== destWells.length) { + if (sourceWells.length === 1) { + sourceWells = Array(destWells.length).fill(sourceWells[0]) + } else if (destWells.length === 1) { + destWells = Array(sourceWells.length).fill(destWells[0]) + } + } + + let disposalVolume = null + let blowoutDestination = null + let blowoutLabware = null + let blowoutWell = null + if (fields.disposalVolume_checkbox || fields.blowout_checkbox) { + if (fields.disposalVolume_checkbox) { + // the disposal volume is only relevant when disposalVolume is checked, + // not when just blowout is checked. + disposalVolume = fields.disposalVolume_volume + } + blowoutDestination = fields.blowout_location + if (blowoutDestination === SOURCE_WELL_BLOWOUT_DESTINATION) { + blowoutLabware = sourceLabware.id + blowoutWell = sourceWells[0] + } else if (blowoutDestination === DEST_WELL_BLOWOUT_DESTINATION) { + blowoutLabware = destLabware.id + blowoutWell = destWells[0] + } else { + // NOTE: if blowoutDestination is not source/dest well, it is a labware ID. + // We are assuming this labware has a well A1, and that both single and multi + // channel pipettes can access that well A1. + blowoutLabware = blowoutDestination + blowoutWell = 'A1' + } + } + const touchTipAfterAspirate = Boolean(fields.aspirate_touchTip_checkbox) - const touchTipAfterAspirateOffsetMmFromBottom = touchTipAfterAspirate - ? fields.aspirate_touchTip_mmFromBottom - : null + + const touchTipAfterAspirateOffsetMmFromBottom = + fields.aspirate_touchTip_mmFromBottom || + getWellsDepth(fields.aspirate_labware.def, sourceWells) + + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP const touchTipAfterDispense = Boolean(fields.dispense_touchTip_checkbox) - const touchTipAfterDispenseOffsetMmFromBottom = touchTipAfterDispense - ? fields.dispense_touchTip_mmFromBottom - : null + const touchTipAfterDispenseOffsetMmFromBottom = + fields.dispense_touchTip_mmFromBottom || + getWellsDepth(fields.dispense_labware.def, destWells) + + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP const mixBeforeAspirate = getMixData( fields, @@ -85,6 +145,8 @@ const moveLiquidFormToArgs = ( const blowoutLocation = (fields.blowout_checkbox && fields.blowout_location) || null + const blowoutOffsetFromTopMm = DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + const commonFields = { pipette: pipetteId, volume, @@ -92,10 +154,17 @@ const moveLiquidFormToArgs = ( sourceLabware: sourceLabware.id, destLabware: destLabware.id, - aspirateFlowRateUlSec: fields.aspirate_flowRate, - dispenseFlowRateUlSec: fields.dispense_flowRate, - aspirateOffsetFromBottomMm: fields.aspirate_mmFromBottom, - dispenseOffsetFromBottomMm: fields.dispense_mmFromBottom, + aspirateFlowRateUlSec: + fields.aspirate_flowRate || pipetteSpec.defaultAspirateFlowRate.value, + dispenseFlowRateUlSec: + fields.dispense_flowRate || pipetteSpec.defaultDispenseFlowRate.value, + aspirateOffsetFromBottomMm: + fields.aspirate_mmFromBottom || DEFAULT_MM_FROM_BOTTOM_ASPIRATE, + dispenseOffsetFromBottomMm: + fields.dispense_mmFromBottom || DEFAULT_MM_FROM_BOTTOM_DISPENSE, + blowoutFlowRateUlSec: + fields.dispense_flowRate || pipetteSpec.defaultDispenseFlowRate.value, + blowoutOffsetFromTopMm, changeTip: fields.changeTip, preWetTip: Boolean(fields.preWetTip), @@ -123,55 +192,6 @@ const moveLiquidFormToArgs = ( }) nor dest (${destWellsUnordered.length}) equal 1` ) - let sourceWells = getOrderedWells( - fields.aspirate_wells, - sourceLabware.def, - fields.aspirate_wellOrder_first, - fields.aspirate_wellOrder_second - ) - - let destWells = getOrderedWells( - fields.dispense_wells, - destLabware.def, - fields.dispense_wellOrder_first, - fields.dispense_wellOrder_second - ) - - // 1:many with single path: spread well array of length 1 to match other well array - if (path === 'single' && sourceWells.length !== destWells.length) { - if (sourceWells.length === 1) { - sourceWells = Array(destWells.length).fill(sourceWells[0]) - } else if (destWells.length === 1) { - destWells = Array(sourceWells.length).fill(destWells[0]) - } - } - - let disposalVolume = null - let blowoutDestination = null - let blowoutLabware = null - let blowoutWell = null - if (fields.disposalVolume_checkbox || fields.blowout_checkbox) { - if (fields.disposalVolume_checkbox) { - // the disposal volume is only relevant when disposalVolume is checked, - // not when just blowout is checked. - disposalVolume = fields.disposalVolume_volume - } - blowoutDestination = fields.blowout_location - if (blowoutDestination === SOURCE_WELL_BLOWOUT_DESTINATION) { - blowoutLabware = sourceLabware.id - blowoutWell = sourceWells[0] - } else if (blowoutDestination === DEST_WELL_BLOWOUT_DESTINATION) { - blowoutLabware = destLabware.id - blowoutWell = destWells[0] - } else { - // NOTE: if blowoutDestination is not source/dest well, it is a labware ID. - // We are assuming this labware has a well A1, and that both single and multi - // channel pipettes can access that well A1. - blowoutLabware = blowoutDestination - blowoutWell = 'A1' - } - } - switch (path) { case 'single': { const transferStepArguments: TransferArgs = { diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.js index f9c8737d4dc..561f47f040c 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.js @@ -1,11 +1,9 @@ // @flow import type { FormData } from '../../../form-types' -import type { DelayArgs } from '../../../step-generation' +import type { PauseArgs } from '../../../step-generation' -type PauseStepArgs = DelayArgs - -const pauseFormToArgs = (formData: FormData): PauseStepArgs => { +const pauseFormToArgs = (formData: FormData): PauseArgs => { const hours = parseFloat(formData['pauseHour']) || 0 const minutes = parseFloat(formData['pauseMinute']) || 0 const seconds = parseFloat(formData['pauseSecond']) || 0 diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/moveLiquidFormToArgs.test.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/moveLiquidFormToArgs.test.js index 69f11a04846..379a6753bcf 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/moveLiquidFormToArgs.test.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/moveLiquidFormToArgs.test.js @@ -1,5 +1,8 @@ // @flow import { getLabwareDefURI } from '@opentrons/shared-data' +import { fixtureP10Single } from '@opentrons/shared-data/pipette/fixtures/name' +import fixture12Trough from '@opentrons/shared-data/labware/fixtures/2/fixture12Trough.json' +import fixture96Plate from '@opentrons/shared-data/labware/fixtures/2/fixture96Plate.json' import moveLiquidFormToArgs, { getMixData, type HydratedMoveLiquidFormData, @@ -11,19 +14,14 @@ import { import { getOrderedWells } from '../../../utils' jest.mock('../../../utils') +const ASPIRATE_WELL = 'A2' // default source is trough for these tests +const DISPENSE_WELL = 'C3' // default dest in 96 flat for these tests + describe('move liquid step form -> command creator args', () => { let hydratedForm: ?HydratedMoveLiquidFormData = null - const sourceLabwareDef = { - version: 123, - namespace: 'foo', - parameters: { loadName: 'sourceLabwareMock' }, - } + const sourceLabwareDef = fixture12Trough const sourceLabwareType = getLabwareDefURI(sourceLabwareDef) - const destLabwareDef = { - version: 123, - namespace: 'foo', - parameters: { loadName: 'destLabwareMock' }, - } + const destLabwareDef = fixture96Plate const destLabwareType = getLabwareDefURI(destLabwareDef) beforeEach(() => { // $FlowFixMe: mock methods @@ -38,7 +36,10 @@ describe('move liquid step form -> command creator args', () => { description: null, fields: { - pipette: { id: 'pipetteId' }, + pipette: { + id: 'pipetteId', + spec: fixtureP10Single, + }, volume: 10, path: 'single', changeTip: 'always', @@ -47,7 +48,7 @@ describe('move liquid step form -> command creator args', () => { type: sourceLabwareType, def: sourceLabwareDef, }, - aspirate_wells: ['B1'], + aspirate_wells: [ASPIRATE_WELL], aspirate_wellOrder_first: 'l2r', aspirate_wellOrder_second: 't2b', aspirate_flowRate: null, @@ -63,7 +64,7 @@ describe('move liquid step form -> command creator args', () => { type: destLabwareType, def: destLabwareDef, }, - dispense_wells: ['B2'], + dispense_wells: [DISPENSE_WELL], dispense_wellOrder_first: 'r2l', dispense_wellOrder_second: 'b2t', dispense_flowRate: null, @@ -90,13 +91,13 @@ describe('move liquid step form -> command creator args', () => { expect(getOrderedWells).toHaveBeenCalledTimes(2) expect(getOrderedWells).toHaveBeenCalledWith( - ['B1'], + [ASPIRATE_WELL], sourceLabwareDef, 'l2r', 't2b' ) expect(getOrderedWells).toHaveBeenCalledWith( - ['B2'], + [DISPENSE_WELL], destLabwareDef, 'r2l', 'b2t' @@ -111,9 +112,9 @@ describe('move liquid step form -> command creator args', () => { volume: 10, changeTip: 'always', sourceLabware: 'sourceLabwareId', - sourceWells: ['B1'], + sourceWells: [ASPIRATE_WELL], destLabware: 'destLabwareId', - destWells: ['B2'], + destWells: [DISPENSE_WELL], }) // no form-specific fields should be passed along @@ -126,14 +127,14 @@ describe('move liquid step form -> command creator args', () => { const checkboxFieldCases = [ { checkboxField: 'aspirate_touchTip_checkbox', - formFields: { aspirate_touchTip_mmFromBottom: 42 }, + formFields: { aspirate_touchTip_mmFromBottom: 101 }, expectedArgsUnchecked: { touchTipAfterAspirate: false, - touchTipAfterAspirateOffsetMmFromBottom: null, + touchTipAfterAspirateOffsetMmFromBottom: 101, }, expectedArgsChecked: { touchTipAfterAspirate: true, - touchTipAfterAspirateOffsetMmFromBottom: 42, + touchTipAfterAspirateOffsetMmFromBottom: 101, }, }, @@ -142,7 +143,7 @@ describe('move liquid step form -> command creator args', () => { formFields: { dispense_touchTip_mmFromBottom: 42 }, expectedArgsUnchecked: { touchTipAfterDispense: false, - touchTipAfterDispenseOffsetMmFromBottom: null, + touchTipAfterDispenseOffsetMmFromBottom: 42, }, expectedArgsChecked: { touchTipAfterDispense: true, @@ -278,7 +279,7 @@ describe('move liquid step form -> command creator args', () => { expect(result).toMatchObject({ disposalVolume: null, disposalLabware: 'sourceLabwareId', - disposalWell: 'B1', + disposalWell: ASPIRATE_WELL, }) }) @@ -296,7 +297,7 @@ describe('move liquid step form -> command creator args', () => { expect(result).toMatchObject({ disposalVolume: null, disposalLabware: 'destLabwareId', - disposalWell: 'B2', + disposalWell: DISPENSE_WELL, }) }) }) diff --git a/protocol-designer/src/steplist/generateSubsteps.js b/protocol-designer/src/steplist/generateSubsteps.js index c661940ece4..4e26f692fc7 100644 --- a/protocol-designer/src/steplist/generateSubsteps.js +++ b/protocol-designer/src/steplist/generateSubsteps.js @@ -27,7 +27,7 @@ import type { ConsolidateArgs, DistributeArgs, MixArgs, - DelayArgs, + PauseArgs, TransferArgs, } from '../step-generation/types' @@ -287,7 +287,7 @@ export function generateSubsteps( if (stepArgs.commandCreatorFnName === 'delay') { // just returns formData - const formData: DelayArgs = stepArgs + const formData: PauseArgs = stepArgs return formData } diff --git a/protocol-designer/src/steplist/substepTimeline.js b/protocol-designer/src/steplist/substepTimeline.js index e9fec89fad0..807378b8601 100644 --- a/protocol-designer/src/steplist/substepTimeline.js +++ b/protocol-designer/src/steplist/substepTimeline.js @@ -18,11 +18,11 @@ function _conditionallyUpdateActiveTips( nextFrame: CommandsAndRobotState ) { const lastNewTipCommand = last( - nextFrame.commands.filter(c => c.command === 'pick-up-tip') + nextFrame.commands.filter(c => c.command === 'pickUpTip') ) const newTipParams = lastNewTipCommand && - lastNewTipCommand.command === 'pick-up-tip' && + lastNewTipCommand.command === 'pickUpTip' && lastNewTipCommand.params if (newTipParams) { diff --git a/protocol-designer/src/steplist/types.js b/protocol-designer/src/steplist/types.js index 26912c9c344..abf06939422 100644 --- a/protocol-designer/src/steplist/types.js +++ b/protocol-designer/src/steplist/types.js @@ -1,5 +1,5 @@ // @flow -import type { DelayArgs, CommandCreatorArgs } from '../step-generation' +import type { PauseArgs, CommandCreatorArgs } from '../step-generation' import type { FormData, StepIdType, @@ -89,7 +89,7 @@ export type SourceDestSubstepItem = | SourceDestSubstepItemSingleChannel | SourceDestSubstepItemMultiChannel -export type SubstepItemData = SourceDestSubstepItem | DelayArgs // Pause substep uses same data as delay args +export type SubstepItemData = SourceDestSubstepItem | PauseArgs // Pause substep uses same data as processed form export type StepItemData = { id: StepIdType, diff --git a/protocol-designer/src/top-selectors/substep-highlight.js b/protocol-designer/src/top-selectors/substep-highlight.js index 4f3a3917806..1222af04657 100644 --- a/protocol-designer/src/top-selectors/substep-highlight.js +++ b/protocol-designer/src/top-selectors/substep-highlight.js @@ -1,9 +1,7 @@ // @flow import { createSelector } from 'reselect' -import { - getWellNamePerMultiTip, - type CommandV1 as Command, -} from '@opentrons/shared-data' +import { getWellNamePerMultiTip } from '@opentrons/shared-data' +import type { Command } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import mapValues from 'lodash/mapValues' @@ -92,7 +90,7 @@ function _getSelectedWellsForStep( } frame.commands.forEach((c: Command) => { - if (c.command === 'pick-up-tip' && c.params.labware === labwareId) { + if (c.command === 'pickUpTip' && c.params.labware === labwareId) { const commandWellName = c.params.well const pipetteId = c.params.pipette const pipetteSpec = diff --git a/protocol-designer/src/top-selectors/tip-contents/index.js b/protocol-designer/src/top-selectors/tip-contents/index.js index f672ae99599..6db212bacff 100644 --- a/protocol-designer/src/top-selectors/tip-contents/index.js +++ b/protocol-designer/src/top-selectors/tip-contents/index.js @@ -11,11 +11,8 @@ import { selectors as stepFormSelectors } from '../../step-forms' import { selectors as stepsSelectors } from '../../ui/steps' import { selectors as fileDataSelectors } from '../../file-data' import { getWellSetForMultichannel } from '../../well-selection/utils' - -import type { - CommandV1 as Command, - LabwareDefinition2, -} from '@opentrons/shared-data' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { Command } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { OutputSelector } from 'reselect' import type { BaseState, Selector } from '../../types' @@ -41,7 +38,7 @@ function getTipHighlighted( ): boolean { const { commands } = commandsAndRobotState const commandUsesTip = (c: Command) => { - if (c.command === 'pick-up-tip' && c.params.labware === labwareId) { + if (c.command === 'pickUpTip' && c.params.labware === labwareId) { const commandWellName = c.params.well const pipetteId = c.params.pipette const pipetteSpec = diff --git a/protocol-designer/src/ui/steps/types.js b/protocol-designer/src/ui/steps/types.js index 3bf6a507dc2..cf40bb7f69f 100644 --- a/protocol-designer/src/ui/steps/types.js +++ b/protocol-designer/src/ui/steps/types.js @@ -1,5 +1,5 @@ // @flow -import type { DelayArgs } from '../../step-generation' +import type { PauseArgs } from '../../step-generation' import type { StepIdType } from '../../form-types' import type { WellIngredientVolumeData, @@ -56,6 +56,6 @@ export type SourceDestSubstepItem = | SourceDestSubstepItemSingleChannel | SourceDestSubstepItemMultiChannel -export type SubstepItemData = SourceDestSubstepItem | DelayArgs // Pause substep uses same data as processed form +export type SubstepItemData = SourceDestSubstepItem | PauseArgs // Pause substep uses same data as processed form export type SubSteps = { [StepIdType]: ?SubstepItemData } diff --git a/protocol-designer/webpack.config.js b/protocol-designer/webpack.config.js index b9b9bcbd915..242eb842969 100644 --- a/protocol-designer/webpack.config.js +++ b/protocol-designer/webpack.config.js @@ -16,7 +16,7 @@ const PROTOCOL_DESIGNER_ENV_VAR_PREFIX = 'OT_PD_' // Also remove all OT_PD_VERSION env vars, the version should always // be gleaned from the package.json -const OT_PD_VERSION = '1.1.1' +const OT_PD_VERSION = '3.0.0' const OT_PD_BUILD_DATE = new Date().toUTCString() const JS_ENTRY = path.join(__dirname, 'src/index.js') diff --git a/shared-data/js/getLabware.js b/shared-data/js/getLabware.js index 967e6743497..20bbaee5491 100644 --- a/shared-data/js/getLabware.js +++ b/shared-data/js/getLabware.js @@ -83,6 +83,3 @@ export function getWellDefsForSVG(labwareName: string) { y: _getSvgYValueForWell(labwareName, wellDef) + yCorrection, })) } - -export const getLabwareDefURI = (def: LabwareDefinition2): string => - `${def.namespace}/${def.parameters.loadName}/${def.version}` diff --git a/shared-data/js/helpers/index.js b/shared-data/js/helpers/index.js index 416aad82bef..de635fec35f 100644 --- a/shared-data/js/helpers/index.js +++ b/shared-data/js/helpers/index.js @@ -1,13 +1,17 @@ // @flow import assert from 'assert' -import { getLabwareDefURI } from '../getLabware' +import uniq from 'lodash/uniq' import type { LabwareDefinition2 } from '../types' + export { canPipetteUseLabware } from './canPipetteUseLabware' export { getWellNamePerMultiTip } from './getWellNamePerMultiTip' export { default as getWellTotalVolume } from './getWellTotalVolume' export { default as wellIsRect } from './wellIsRect' export * from './volume' +export const getLabwareDefURI = (def: LabwareDefinition2): string => + `${def.namespace}/${def.parameters.loadName}/${def.version}` + export const getLabwareDisplayName = (labwareDef: LabwareDefinition2) => labwareDef.metadata.displayName @@ -102,3 +106,24 @@ export function splitWellsOnColumn( } }, []) } + +// NOTE: this is used in PD for converting "offset from top" to "mm from bottom". +// Assumes all wells have same offset because multi-offset not yet supported. +// TODO: Ian 2019-07-13 return {[string: well]: offset} to support multi-offset +export const getWellsDepth = ( + labwareDef: LabwareDefinition2, + wells: Array +): number => { + const offsets = wells.map(well => labwareDef.wells[well].depth) + if (uniq(offsets).length !== 1) { + console.warn( + `expected wells ${JSON.stringify( + wells + )} to all have same offset, but they were different. Labware def is ${getLabwareDefURI( + labwareDef + )}` + ) + } + + return offsets[0] +} diff --git a/shared-data/js/pipettes.js b/shared-data/js/pipettes.js index 8a31b706b77..b2805be4f93 100644 --- a/shared-data/js/pipettes.js +++ b/shared-data/js/pipettes.js @@ -1,5 +1,4 @@ // @flow -import reduce from 'lodash/reduce' import pipetteNameSpecs from '../pipette/definitions/pipetteNameSpecs.json' import pipetteModelSpecs from '../pipette/definitions/pipetteModelSpecs.json' @@ -69,16 +68,3 @@ function comparePipettes(sortBy: Array) { return 0 } } - -export function getFlowRateDefaultsAllPipettes( - flowRateName: 'defaultAspirateFlowRate' | 'defaultDispenseFlowRate' -): { [pipetteName: string]: number } { - return reduce( - pipetteNameSpecs, - (acc, spec: PipetteNameSpecs, pipetteName: string) => ({ - ...acc, - [pipetteName]: spec[flowRateName].value, - }), - {} - ) -} diff --git a/shared-data/protocol/flowTypes/schemaV1.js b/shared-data/protocol/flowTypes/schemaV1.js index 8d1b2416392..91cc23976b9 100644 --- a/shared-data/protocol/flowTypes/schemaV1.js +++ b/shared-data/protocol/flowTypes/schemaV1.js @@ -4,32 +4,32 @@ import type { DeckSlotId } from '@opentrons/shared-data' // COMMANDS -export type PipetteLabwareFieldsV1 = {| +export type PipetteLabwareFields = {| pipette: string, labware: string, well: string, |} -export type AspirateDispenseArgsV1 = {| - ...PipetteLabwareFieldsV1, +export type AspirateDispenseArgs = {| + ...PipetteLabwareFields, volume: number, offsetFromBottomMm?: ?number, 'flow-rate'?: ?number, |} -export type CommandV1 = +export type Command = | {| command: 'aspirate' | 'dispense', - params: AspirateDispenseArgsV1, + params: AspirateDispenseArgs, |} | {| command: 'pick-up-tip' | 'drop-tip' | 'blowout', - params: PipetteLabwareFieldsV1, + params: PipetteLabwareFields, |} | {| command: 'touch-tip', params: {| - ...PipetteLabwareFieldsV1, + ...PipetteLabwareFields, offsetFromBottomMm?: ?number, |}, |} @@ -58,13 +58,13 @@ type VersionString = string // eg '1.0.0' type PipetteModel = string type PipetteName = string -export type FilePipetteV1 = { +export type FilePipette = { mount: Mount, model: PipetteModel, name?: PipetteName, } -export type FileLabwareV1 = { +export type FileLabware = { slot: DeckSlotId, model: string, 'display-name'?: string, @@ -75,7 +75,7 @@ type FlowRateForPipettes = { } // A v1 JSON protocol file -export type SchemaV1ProtocolFile = { +export type ProtocolFile = { 'protocol-schema': VersionString, metadata: { @@ -109,11 +109,11 @@ export type SchemaV1ProtocolFile = { }, pipettes: { - [instrumentId: string]: FilePipetteV1, + [instrumentId: string]: FilePipette, }, labware: { - [labwareId: string]: FileLabwareV1, + [labwareId: string]: FileLabware, }, procedure: Array<{ @@ -121,6 +121,6 @@ export type SchemaV1ProtocolFile = { name: string, description: string, }, - subprocedure: Array, + subprocedure: Array, }>, } diff --git a/shared-data/protocol/flowTypes/schemaV3.js b/shared-data/protocol/flowTypes/schemaV3.js index 032399e84bb..68e1142beeb 100644 --- a/shared-data/protocol/flowTypes/schemaV3.js +++ b/shared-data/protocol/flowTypes/schemaV3.js @@ -5,15 +5,15 @@ import type { DeckSlotId, LabwareDefinition2 } from '@opentrons/shared-data' // NOTE: this is an enum type in the spec, but it's inconvenient to flow-type them. type PipetteName = string -export type FilePipetteV3 = { +export type FilePipette = { mount: Mount, name: PipetteName, } -export type FileLabwareV3 = { +export type FileLabware = { slot: DeckSlotId, - labwareDefURI: string, - 'display-name'?: string, + definitionId: string, + displayName?: string, } type FlowRateParams = {| flowRate: number |} @@ -24,30 +24,59 @@ type VolumeParams = {| volume: number |} type OffsetParams = {| offsetFromBottomMm: number |} -export type CommandV3 = +type _AspDispAirgapParams = {| + ...FlowRateParams, + ...PipetteAccessParams, + ...VolumeParams, + ...OffsetParams, +|} + +export type AspirateParams = _AspDispAirgapParams +export type DispenseParams = _AspDispAirgapParams +export type AirGapParams = _AspDispAirgapParams + +export type BlowoutParams = {| + ...FlowRateParams, + ...PipetteAccessParams, + ...OffsetParams, +|} + +export type TouchTipParams = {| + ...PipetteAccessParams, + ...OffsetParams, +|} + +export type PickUpTipParams = PipetteAccessParams +export type DropTipParams = PipetteAccessParams + +export type MoveToSlotParams = {| + pipette: string, + slot: string, + offset?: {| + x: number, + y: number, + z: number, + |}, + minimumZHeight: number, +|} + +export type DelayParams = {| + wait: number | true, + message?: string, +|} + +export type Command = | {| command: 'aspirate' | 'dispense' | 'airGap', - params: {| - ...FlowRateParams, - ...PipetteAccessParams, - ...VolumeParams, - ...OffsetParams, - |}, + params: _AspDispAirgapParams, |} | {| command: 'blowout', - params: {| - ...FlowRateParams, - ...PipetteAccessParams, - ...OffsetParams, - |}, + params: BlowoutParams, |} | {| command: 'touchTip', - params: {| - ...PipetteAccessParams, - ...OffsetParams, - |}, + params: TouchTipParams, |} | {| command: 'pickUpTip' | 'dropTip', @@ -55,27 +84,15 @@ export type CommandV3 = |} | {| command: 'moveToSlot', - params: {| - pipette: string, - slot: string, - offset?: {| - x: number, - y: number, - z: number, - |}, - minimumZHeight: number, - |}, + params: MoveToSlotParams, |} | {| command: 'delay', - params: {| - wait: number | true, - message?: string, - |}, + params: DelayParams, |} // NOTE: must be kept in sync with '../schemas/3.json' -export type SchemaV3ProtocolFile = {| +export type ProtocolFile = {| schemaVersion: 3, metadata: { protocolName?: string, @@ -96,7 +113,7 @@ export type SchemaV3ProtocolFile = {| model: 'OT-2 Standard', }, pipettes: { - [pipetteId: string]: FilePipetteV3, + [pipetteId: string]: FilePipette, }, labwareDefinitions: { [labwareDefId: string]: LabwareDefinition2, @@ -108,6 +125,6 @@ export type SchemaV3ProtocolFile = {| displayName?: string, }, }, - commands: Array, + commands: Array, commandAnnotations?: Object, // NOTE: intentionally underspecified b/c we haven't decided on this yet |} diff --git a/shared-data/protocol/index.js b/shared-data/protocol/index.js index 1b59f446275..d0da7d5d2f6 100644 --- a/shared-data/protocol/index.js +++ b/shared-data/protocol/index.js @@ -1,6 +1,6 @@ // @flow -import type { SchemaV1ProtocolFile } from './flowTypes/schemaV1' -import type { SchemaV3ProtocolFile } from './flowTypes/schemaV3' +import type { ProtocolFile as SchemaV1ProtocolFile } from './flowTypes/schemaV1' +import type { ProtocolFile as SchemaV3ProtocolFile } from './flowTypes/schemaV3' type ProtocolData = | $Shape> @@ -20,6 +20,3 @@ export function getProtocolSchemaVersion(data: ProtocolData): ?number { } return null } - -export * from './flowTypes/schemaV1' -export * from './flowTypes/schemaV3' diff --git a/shared-data/protocol/schemas/3.json b/shared-data/protocol/schemas/3.json index 06de777dddb..e29e32d19c2 100644 --- a/shared-data/protocol/schemas/3.json +++ b/shared-data/protocol/schemas/3.json @@ -57,8 +57,9 @@ "required": ["flowRate"], "properties": { "flowRate": { - "description": "Flow rate in uL/sec", - "type": "number" + "description": "Flow rate in uL/sec. Must be greater than 0", + "type": "number", + "minimum": 0 } } },