Skip to content

Commit

Permalink
feat(protocol-designer): save v3 protocols (#3588)
Browse files Browse the repository at this point in the history
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'`
  • Loading branch information
IanLondon authored Jun 18, 2019
1 parent 851edf4 commit 40f3a9e
Show file tree
Hide file tree
Showing 79 changed files with 1,929 additions and 1,801 deletions.
58 changes: 12 additions & 46 deletions api/src/opentrons/protocols/execute_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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']
Expand All @@ -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:
Expand All @@ -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')
Expand Down Expand Up @@ -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
Expand Down
35 changes: 13 additions & 22 deletions api/tests/opentrons/protocols/test_execute_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ def test_get_location():
plate = labware.load("96-flat", 1)
well = "B2"

default_values = {
'aspirateMmFromBottom': 2
}

loaded_labware = {
"someLabwareId": plate
}
Expand All @@ -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()
Expand Down Expand Up @@ -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": {
Expand All @@ -171,7 +159,8 @@ def mock_set_flow_rate(aspirate, dispense):
"labware": "sourcePlateId",
"well": "A1",
"volume": 5,
"flowRate": 123
"flowRate": 123,
"offsetFromBottomMm": aspirateOffset
}
},
{
Expand All @@ -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
}
},
]
Expand All @@ -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)
]
6 changes: 2 additions & 4 deletions app/src/protocol/types.js
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default connect<Props, {||}, SP, {||}, _, _>(

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'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const DECIMALS_ALLOWED = 1

type OP = {|
mmFromBottom: number,
wellHeightMM: number,
wellDepthMm: number,
isOpen: boolean,
closeModal: () => mixed,
fieldName: TipOffsetFields,
Expand All @@ -57,7 +57,7 @@ class TipPositionModal extends React.Component<Props, State> {
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) })
}
}
Expand All @@ -67,21 +67,21 @@ class TipPositionModal extends React.Component<Props, State> {
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,
minMmFromBottom: number,
} => {
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,
}
}
Expand Down Expand Up @@ -139,7 +139,7 @@ class TipPositionModal extends React.Component<Props, State> {
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 (
Expand Down Expand Up @@ -209,7 +209,7 @@ class TipPositionModal extends React.Component<Props, State> {
mmFromBottom={
value != null ? value : this.getDefaultMmFromBottom()
}
wellHeightMM={wellHeightMM}
wellDepthMm={wellDepthMm}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -29,8 +29,8 @@ const TipPositionZAxisViz = (props: Props) => {
className={styles.pipette_tip_image}
style={{ bottom: `${bottomPx}px` }}
/>
{props.wellHeightMM !== null && (
<span className={styles.well_height_label}>{props.wellHeightMM}mm</span>
{props.wellDepthMm !== null && (
<span className={styles.well_height_label}>{props.wellDepthMm}mm</span>
)}
<img
src={WELL_CROSS_SECTION_IMAGE}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import * as React from 'react'
import { connect } from 'react-redux'
import { HoverTooltip, FormGroup, InputField } from '@opentrons/components'
import { getWellsDepth } from '@opentrons/shared-data'
import i18n from '../../../../localization'
import { selectors as stepFormSelectors } from '../../../../step-forms'
import { getDisabledFields } from '../../../../steplist/formLevel'
Expand Down Expand Up @@ -32,7 +33,7 @@ type OP = {| fieldName: TipOffsetFields, className?: string |}
type SP = {|
disabled: boolean,
mmFromBottom: ?string,
wellHeightMM: ?number,
wellDepthMm: ?number,
|}

type Props = { ...OP, ...SP }
Expand All @@ -43,7 +44,7 @@ class TipPositionInput extends React.Component<Props, TipPositionInputState> {
state: TipPositionInputState = { isModalOpen: false }

handleOpen = () => {
if (this.props.wellHeightMM) {
if (this.props.wellDepthMm) {
this.setState({ isModalOpen: true })
}
}
Expand All @@ -52,7 +53,7 @@ class TipPositionInput extends React.Component<Props, TipPositionInputState> {
}

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 }) =>
Expand All @@ -70,12 +71,12 @@ class TipPositionInput extends React.Component<Props, TipPositionInputState> {
)

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 (
Expand All @@ -87,7 +88,7 @@ class TipPositionInput extends React.Component<Props, TipPositionInputState> {
<TipPositionModal
fieldName={fieldName}
closeModal={this.handleClose}
wellHeightMM={wellHeightMM}
wellDepthMm={wellDepthMm}
// $FlowFixMe: mmFromBottom is typed as a number in TipPositionModal
mmFromBottom={mmFromBottom}
isOpen={this.state.isModalOpen}
Expand All @@ -113,20 +114,20 @@ const mapSTP = (state: BaseState, ownProps: OP): SP => {
ownProps.fieldName
)

let wellHeightMM = null
let wellDepthMm = null
const labwareId: ?string = rawForm && rawForm[labwareFieldName]
if (labwareId != null) {
const labwareDef = stepFormSelectors.getLabwareEntities(state)[labwareId]
.def

// 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],
}
}
Expand Down
Loading

0 comments on commit 40f3a9e

Please sign in to comment.