diff --git a/api/opentrons/protocols/__init__.py b/api/opentrons/protocols/__init__.py index 8a80a287cd5..2c508e937c2 100644 --- a/api/opentrons/protocols/__init__.py +++ b/api/opentrons/protocols/__init__.py @@ -56,8 +56,16 @@ def load_labware(protocol_data): def get_location(command_params, loaded_labware): labwareId = command_params.get('labware') + if not labwareId: + # not all commands use labware param + return None well = command_params.get('well') - return loaded_labware.get(labwareId, {}).get(well) + labware = loaded_labware.get(labwareId) + if not labware: + raise ValueError( + 'Command tried to use labware "{}", but that ID does not exist ' + + 'in protocol\'s "labware" section'.format(labwareId)) + return labware.wells(well) def get_pipette(command_params, loaded_pipettes): @@ -65,12 +73,47 @@ def get_pipette(command_params, loaded_pipettes): return loaded_pipettes.get(pipetteId) +# 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_model, pipette, command_type, params, default_values): + """ + Set flow rate in uL/mm, to value obtained from command's params, + or if unspecified in command params, then from protocol's "default-values". + """ + default_aspirate = default_values.get( + 'aspirate-flow-rate', {}).get(pipette_model) + + default_dispense = default_values.get( + 'dispense-flow-rate', {}).get(pipette_model) + + flow_rate_param = params.get('flow-rate') + + 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 + ) + + # 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 subprocedures = [ p.get('subprocedure', []) for p in protocol_data.get('procedure', [])] + default_values = protocol_data.get('default-values', {}) flat_subs = chain.from_iterable(subprocedures) for command_item in flat_subs: @@ -78,9 +121,22 @@ def dispatch_commands(protocol_data, loaded_pipettes, loaded_labware): # noqa: params = command_item.get('params', {}) pipette = get_pipette(params, loaded_pipettes) + pipette_model = protocol_data\ + .get('pipettes', {})\ + .get(params.get('pipette'), {})\ + .get('model') + location = get_location(params, loaded_labware) volume = params.get('volume') + if pipette: + # Aspirate/Dispense flow rate must be set each time for commands + # which use pipettes right now. + # Flow rate is persisted inside the Pipette object + # and is settable but not easily gettable + set_flow_rate( + pipette_model, pipette, command_type, params, default_values) + if command_type == 'delay': wait = params.get('wait', 0) if wait is True: diff --git a/api/tests/opentrons/protocols/test_load_json_protocol.py b/api/tests/opentrons/protocols/test_load_json_protocol.py index 9192a399519..6532682a35e 100644 --- a/api/tests/opentrons/protocols/test_load_json_protocol.py +++ b/api/tests/opentrons/protocols/test_load_json_protocol.py @@ -70,6 +70,7 @@ def test_blank_protocol(): def test_dispatch_commands(monkeypatch): robot.reset() cmd = [] + flow_rates = [] def mock_sleep(seconds): cmd.append(("sleep", seconds)) @@ -80,6 +81,9 @@ def mock_aspirate(volume, location): def mock_dispense(volume, location): cmd.append(("dispense", volume, location)) + def mock_set_flow_rate(aspirate, dispense): + flow_rates.append((aspirate, dispense)) + pipette = instruments.P10_Single('left') loaded_pipettes = { @@ -96,9 +100,24 @@ def mock_dispense(volume, location): monkeypatch.setattr(pipette, 'aspirate', mock_aspirate) monkeypatch.setattr(pipette, 'dispense', mock_dispense) + monkeypatch.setattr(pipette, 'set_flow_rate', mock_set_flow_rate) monkeypatch.setattr(protocols, '_sleep', mock_sleep) protocol_data = { + "default-values": { + "aspirate-flow-rate": { + "p300_single_v1": 101 + }, + "dispense-flow-rate": { + "p300_single_v1": 102 + } + }, + "pipettes": { + "pipetteId": { + "mount": "left", + "model": "p300_single_v1" + } + }, "procedure": [ { "subprocedure": [ @@ -108,7 +127,8 @@ def mock_dispense(volume, location): "pipette": "pipetteId", "labware": "sourcePlateId", "well": "A1", - "volume": 5 + "volume": 5, + "flow-rate": 123 } }, { @@ -139,3 +159,8 @@ def mock_dispense(volume, location): ("sleep", 42), ("dispense", 4.5, dest_plate['B1']) ] + + assert flow_rates == [ + (123, 102), + (101, 102) + ] diff --git a/protocol-designer/src/file-data/selectors/fileCreator.js b/protocol-designer/src/file-data/selectors/fileCreator.js index c6281af8377..6d4802f5457 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.js +++ b/protocol-designer/src/file-data/selectors/fileCreator.js @@ -1,6 +1,7 @@ // @flow import {createSelector} from 'reselect' import mapValues from 'lodash/mapValues' +import {getPropertyAllPipettes} from '@opentrons/shared-data' import {fileMetadata} from './fileFields' import {getInitialRobotState, robotStateTimeline} from './commands' import {selectors as dismissSelectors} from '../../dismiss' @@ -14,6 +15,11 @@ import type {LabwareData, PipetteData} from '../../step-generation' const protocolSchemaVersion = '1.0.0' const applicationVersion = process.env.OT_PD_VERSION || 'unknown version' +const executionDefaults = { + 'aspirate-flow-rate': getPropertyAllPipettes('aspirateFlowRate'), + 'dispense-flow-rate': getPropertyAllPipettes('dispenseFlowRate') +} + export const createFile: BaseState => ProtocolFile = createSelector( fileMetadata, getInitialRobotState, @@ -73,6 +79,8 @@ export const createFile: BaseState => ProtocolFile = createSelector( tags: [] }, + 'default-values': executionDefaults, + 'designer-application': { 'application-name': 'opentrons/protocol-designer', 'application-version': applicationVersion, diff --git a/protocol-designer/src/file-types.js b/protocol-designer/src/file-types.js index 3986d06943c..f100ecd4781 100644 --- a/protocol-designer/src/file-types.js +++ b/protocol-designer/src/file-types.js @@ -7,10 +7,11 @@ import type {RootState as DismissRoot} from './dismiss' type MsSinceEpoch = number type VersionString = string // eg '1.0.0' +type PipetteModel = string // TODO Ian 2018-05-11 use pipette-definitions model types enum export type FilePipette = { mount: Mount, - model: string // TODO Ian 2018-05-11 use pipette-definitions model types + model: PipetteModel } export type FileLabware = { @@ -19,6 +20,10 @@ export type FileLabware = { 'display-name': string } +export type FlowRateForPipettes = { + [PipetteModel]: number +} + export type PDMetadata = { // pipetteId to tiprackModel. may be unassigned pipetteTiprackAssignments: {[pipetteId: string]: ?string}, @@ -48,6 +53,11 @@ export type ProtocolFile = { tags: Array }, + 'default-values': { + 'aspirate-flow-rate': FlowRateForPipettes, + 'dispense-flow-rate': FlowRateForPipettes + }, + 'designer-application': { 'application-name': 'opentrons/protocol-designer', 'application-version': VersionString, diff --git a/shared-data/js/pipettes.js b/shared-data/js/pipettes.js index 3e34ffecb42..b87c6d21e08 100644 --- a/shared-data/js/pipettes.js +++ b/shared-data/js/pipettes.js @@ -1,4 +1,5 @@ // @flow +import mapValues from 'lodash/mapValues' import pipetteConfigByModel from '../robot-data/pipette-config.json' export type PipetteChannels = 1 | 8 @@ -94,3 +95,7 @@ function comparePipettes (sortBy: Array) { return 0 } } + +export function getPropertyAllPipettes (propertyName: $Keys) { + return mapValues(pipetteConfigByModel, config => config[propertyName]) +} diff --git a/shared-data/protocol-json-schema/protocol-schema.json b/shared-data/protocol-json-schema/protocol-schema.json index 91a9e30fd4b..c0d5ce5a4b7 100644 --- a/shared-data/protocol-json-schema/protocol-schema.json +++ b/shared-data/protocol-json-schema/protocol-schema.json @@ -2,13 +2,55 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "pipette-model": { + "description": "Special keyword specifying the model of the pipette. TODO finalize these model names they are still TBD", + "type": "string", + "enum": [ + "p10_single_v1", + "p10_multi_v1", + "p50_single_v1", + "p50_multi_v1", + "p300_single_v1", + "p300_multi_v1", + "p1000_single_v1", + "p1000_multi_v1", + + "p10_single_v1.3", + "p10_multi_v1.3", + "p50_single_v1.3", + "p50_multi_v1.3", + "p300_single_v1.3", + "p300_multi_v1.3", + "p1000_single_v1.3", + "p1000_multi_v1.3" + ] + }, + "mm-offset": { "description": "Millimeters for pipette location offsets", "type": "number" }, + "flow-rate-for-pipettes": { + "description": "Flow rate in mm/sec for each pipette model used in the protocol", + "type": "object", + "propertyNames": {"$ref": "#/definitions/pipette-model"}, + "patternProperties": {".*": {"type": "number"}}, + "additionalProperties": false + }, + + "flow-rate-params": { + "properties": { + "flow-rate": { + "description": "Flow rate for aspirate/dispense. If omitted, defaults to the corresponding values in \"default-values\"", + "type": "number" + } + } + }, + "well-position-params": { "description": "Optional params for well position offsets", + "type": "object", "properties": { "position": { "required": ["anchor"], @@ -61,6 +103,7 @@ "additionalProperties": false, "required": [ "protocol-schema", + "default-values", "metadata", "robot", "pipettes", @@ -118,6 +161,19 @@ } }, + "default-values": { + "description": "Default values required for protocol execution", + "type": "object", + "required": [ + "aspirate-flow-rate", + "dispense-flow-rate" + ], + "properties": { + "aspirate-flow-rate": {"$ref": "#/definitions/flow-rate-for-pipettes"}, + "dispense-flow-rate": {"$ref": "#/definitions/flow-rate-for-pipettes"} + } + }, + "designer-application": { "description": "Optional data & metadata not required to execute the protocol, used by the application that created this protocol", "type": "object", @@ -163,18 +219,7 @@ "enum": ["left", "right"] }, "model": { - "description": "Special keyword specifying the model of the pipette. TODO finalize these model names they are still TBD", - "type": "string", - "enum": [ - "p10_single_v1", - "p10_multi_v1", - "p50_single_v1", - "p50_multi_v1", - "p300_single_v1", - "p300_multi_v1", - "p1000_single_v1", - "p1000_multi_v1" - ] + "$ref": "#/definitions/pipette-model" } } } @@ -232,7 +277,8 @@ "subprocedure": { "type": "array", "items": { - "anyOf": [{ + "anyOf": [ + { "description": "Aspirate / dispense / air gap commands", "type": "object", "required": ["command", "params"], @@ -243,6 +289,7 @@ }, "params": { "allOf": [ + {"$ref": "#/definitions/flow-rate-params"}, {"$ref": "#/definitions/pipette-access-params"}, {"$ref": "#/definitions/volume-params"}, {"$ref": "#/definitions/well-position-params"}