Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol-designer): highlight tips per substep #2716

Merged
merged 3 commits into from
Nov 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ type TransferLikeStepArgs = ConsolidateFormData | DistributeFormData | TransferF

// TODO: BC 2018-10-30 move getting labwareDef into hydration layer upstream
const transferLikeFormToArgs = (hydratedFormData: FormData): TransferLikeStepArgs => {
console.log([hydratedFormData])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎈

const stepType = hydratedFormData.stepType
const pipette = hydratedFormData['pipette']
const volume = Number(hydratedFormData['volume'])
Expand Down
12 changes: 10 additions & 2 deletions protocol-designer/src/steplist/generateSubsteps.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ function transferLikeSubsteps (args: {
preIngreds: nextMultiRow.dest.preIngreds[destChannelWell],
postIngreds: nextMultiRow.dest.postIngreds[destChannelWell],
}
const activeTips = currentMultiRow.activeTips
return {
activeTips,
source,
dest: stepArgs.stepType === 'mix' ? source : dest, // NOTE: since source and dest are same for mix, we're showing source on both sides. Otherwise dest would show the intermediate volume state
volume: showDispenseVol ? nextMultiRow.volume : currentMultiRow.volume,
Expand All @@ -158,7 +160,8 @@ function transferLikeSubsteps (args: {
preIngreds: currentMultiRow.dest.preIngreds[currentMultiRow.dest.wells[channelIndex]],
postIngreds: currentMultiRow.dest.postIngreds[currentMultiRow.dest.wells[channelIndex]],
}
return { source, dest, volume: currentMultiRow.volume }
const activeTips = currentMultiRow.activeTips
return {activeTips, source, dest, volume: currentMultiRow.volume}
})
)
)
Expand Down Expand Up @@ -204,7 +207,12 @@ function transferLikeSubsteps (args: {
preIngreds: currentRow.dest.preIngreds,
postIngreds: currentRow.dest.postIngreds,
}
return {source, dest, volume: currentRow.volume}
return {
activeTips: currentRow.activeTips,
source,
dest,
volume: currentRow.volume,
}
}
)

Expand Down
116 changes: 87 additions & 29 deletions protocol-designer/src/steplist/substepTimeline.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,86 @@
// @flow
import assert from 'assert'
import last from 'lodash/last'
import pick from 'lodash/pick'
import type {Channels} from '@opentrons/components'
import type {CommandCreator, RobotState, CommandCreatorError} from '../step-generation/types'
import type {
CommandCreator,
CommandCreatorError,
CommandsAndRobotState,
RobotState,
} from '../step-generation/types'

import {getWellsForTips} from '../step-generation/utils'
import type {SubstepTimelineFrame} from './types'
import type {SubstepTimelineFrame, TipLocation} from './types'

type SubstepTimeline = {
function _conditionallyUpdateActiveTips (acc: SubstepTimelineAcc, nextFrame: CommandsAndRobotState) {
const lastNewTipCommand = last(nextFrame.commands.filter(c => c.command === 'pick-up-tip'))
const newTipParams = lastNewTipCommand &&
lastNewTipCommand.command === 'pick-up-tip' &&
lastNewTipCommand.params

if (newTipParams) {
return {...acc, prevActiveTips: {...newTipParams}}
}
return acc
}

type SubstepTimelineAcc = {
timeline: Array<SubstepTimelineFrame>,
errors: ?Array<CommandCreatorError>,
prevActiveTips: ?TipLocation,
prevRobotState: RobotState,
}

const substepTimelineSingle = (commandCreators: Array<CommandCreator>) =>
(initialRobotState: RobotState): Array<SubstepTimelineFrame> => {
let prevRobotState = initialRobotState
const timeline = commandCreators.reduce((acc: SubstepTimeline, commandCreator: CommandCreator, index: number) => {
const timeline = commandCreators.reduce((acc: SubstepTimelineAcc, commandCreator: CommandCreator, index: number) => {
// error short-circuit
if (acc.errors) return acc

const nextFrame = commandCreator(prevRobotState)
const nextFrame = commandCreator(acc.prevRobotState)

if (nextFrame.errors) {
return {timeline: acc.timeline, errors: nextFrame.errors}
return {...acc, errors: nextFrame.errors}
}

// NOTE: only aspirate and dispense commands will appear alone in atomic commands
// from compound command creators (e.g. transfer, distribute, etc.)
const isAtomic = nextFrame.commands.length === 1
const commandGroup = nextFrame.commands[0]
if (isAtomic && (commandGroup.command === 'aspirate' || commandGroup.command === 'dispense')) {
const firstCommand = nextFrame.commands[0]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commandGroup is really a single Command object not a group, so I renamed this for multi and single

if (
firstCommand.command === 'aspirate' ||
firstCommand.command === 'dispense') {
assert(nextFrame.commands.length === 1,
`substepTimeline expected nextFrame to have only single commands for ${firstCommand.command}`)

const commandGroup = firstCommand
const {well, volume, labware} = commandGroup.params
const wellInfo = {
labware,
wells: [well],
preIngreds: prevRobotState.liquidState.labware[labware][well],
preIngreds: acc.prevRobotState.liquidState.labware[labware][well],
postIngreds: nextFrame.robotState.liquidState.labware[labware][well],
}
prevRobotState = nextFrame.robotState
const ingredKey = commandGroup.command === 'aspirate' ? 'source' : 'dest'
return {timeline: [...acc.timeline, {volume, [ingredKey]: wellInfo}], errors: null}
return {
...acc,
timeline: [
...acc.timeline,
{
volume,
[ingredKey]: wellInfo,
activeTips: acc.prevActiveTips,
},
],
prevRobotState: nextFrame.robotState,
}
} else {
return acc
return {
..._conditionallyUpdateActiveTips(acc, nextFrame),
prevRobotState: nextFrame.robotState,
}
}
}, {timeline: [], errors: null})
}, {timeline: [], errors: null, prevActiveTips: null, prevRobotState: initialRobotState})

return timeline.timeline
}
Expand All @@ -54,38 +93,57 @@ const substepTimeline = (
if (context.channels === 1) {
return substepTimelineSingle(commandCreators)
} else {
// timeline for multi-channel substep context
return (
(initialRobotState: RobotState): Array<SubstepTimelineFrame> => {
let prevRobotState = initialRobotState
const timeline = commandCreators.reduce((acc: SubstepTimeline, commandCreator: CommandCreator, index: number) => {
const timeline = commandCreators.reduce((acc: SubstepTimelineAcc, commandCreator: CommandCreator, index: number) => {
// error short-circuit
if (acc.errors) return acc

const nextFrame = commandCreator(prevRobotState)
const nextFrame = commandCreator(acc.prevRobotState)

if (nextFrame.errors) {
return {timeline: acc.timeline, errors: nextFrame.errors}
return {...acc, errors: nextFrame.errors}
}

const isAtomic = nextFrame.commands.length === 1
const commandGroup = nextFrame.commands[0]
if (isAtomic && (commandGroup.command === 'aspirate' || commandGroup.command === 'dispense')) {
const {well, volume, labware} = commandGroup.params
const firstCommand = nextFrame.commands[0]
if (
firstCommand.command === 'aspirate' ||
firstCommand.command === 'dispense'
) {
assert(nextFrame.commands.length === 1,
`substepTimeline expected nextFrame to have only single commands for ${firstCommand.command}`)

const {well, volume, labware} = firstCommand.params
const labwareType = context.getLabwareType && context.getLabwareType(labware)
const wellsForTips = context.channels && labwareType && getWellsForTips(context.channels, labwareType, well).wellsForTips
const wellInfo = {
labware,
wells: wellsForTips || [],
preIngreds: wellsForTips ? pick(prevRobotState.liquidState.labware[labware], wellsForTips) : {},
preIngreds: wellsForTips ? pick(acc.prevRobotState.liquidState.labware[labware], wellsForTips) : {},
postIngreds: wellsForTips ? pick(nextFrame.robotState.liquidState.labware[labware], wellsForTips) : {},
}
prevRobotState = nextFrame.robotState
const ingredKey = commandGroup.command === 'aspirate' ? 'source' : 'dest'
return {timeline: [...acc.timeline, {volume, [ingredKey]: wellInfo}], errors: null}

const ingredKey = firstCommand.command === 'aspirate' ? 'source' : 'dest'
return {
...acc,
timeline: [
...acc.timeline,
{
volume,
[ingredKey]: wellInfo,
activeTips: acc.prevActiveTips,
},
],
prevRobotState: nextFrame.robotState,
}
} else {
return acc
return {
..._conditionallyUpdateActiveTips(acc, nextFrame),
prevRobotState: nextFrame.robotState,
}
}
}, {timeline: [], errors: null})
}, {timeline: [], errors: null, prevActiveTips: null, prevRobotState: initialRobotState})

return timeline.timeline
}
Expand Down
4 changes: 3 additions & 1 deletion protocol-designer/src/steplist/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export const END_TERMINAL_ITEM_ID: '__end__' = '__end__'
export type TerminalItemId = typeof START_TERMINAL_ITEM_ID | typeof END_TERMINAL_ITEM_ID

export type WellIngredientNames = {[ingredId: string]: string}

export type WellIngredientVolumeData = {[ingredId: string]: {volume: number}}
export type TipLocation = {labware: string, well: string}

export type SubstepIdentifier = {|
stepId: StepIdType,
Expand All @@ -41,6 +41,7 @@ export type SourceDestData = {

export type SubstepTimelineFrame = {
substepIndex?: number,
activeTips: ?TipLocation,
source?: SourceDestData,
dest?: SourceDestData,
volume?: ?number,
Expand All @@ -54,6 +55,7 @@ export type SubstepWellData = {
}

export type StepItemSourceDestRow = {
activeTips: ?TipLocation,
substepIndex?: number,
source?: SubstepWellData,
dest?: SubstepWellData,
Expand Down
3 changes: 2 additions & 1 deletion protocol-designer/src/top-selectors/substeps.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ export const allSubsteps: Selector<AllSubsteps> = createSelector(
robotStateTimeline,
_initialRobotState,
) => {
const timeline = [{robotState: _initialRobotState}, ...robotStateTimeline.timeline]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this timeline was being created new inside each loop of the reduce, I think it was just a mistake?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch


return orderedSteps.reduce((acc: AllSubsteps, stepId, timelineIndex) => {
const timeline = [{robotState: _initialRobotState}, ...robotStateTimeline.timeline]
const robotState = timeline[timelineIndex] && timeline[timelineIndex].robotState

const substeps = generateSubsteps(
Expand Down
49 changes: 41 additions & 8 deletions protocol-designer/src/top-selectors/tip-contents/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {createSelector} from 'reselect'
import noop from 'lodash/noop'
import * as StepGeneration from '../../step-generation'
import {allSubsteps as getAllSubsteps} from '../substeps'
import {
selectors as steplistSelectors,
START_TERMINAL_ITEM_ID,
Expand Down Expand Up @@ -32,16 +33,16 @@ function getTipHighlighted (
if (c.command === 'pick-up-tip' && c.params.labware === labwareId) {
const commandWellName = c.params.well
const pipetteId = c.params.pipette
const labwareName = StepGeneration.getLabwareType(labwareId, robotState)
const labwareType = StepGeneration.getLabwareType(labwareId, robotState)
const channels = StepGeneration.getPipetteChannels(pipetteId, robotState)

if (!labwareName) {
console.error(`Labware ${labwareId} missing labwareName. Could not get tip highlight state`)
if (!labwareType) {
console.error(`Labware ${labwareId} missing labwareType. Could not get tip highlight state`)
return false
} else if (channels === 1) {
return commandWellName === wellName
} else if (channels === 8) {
const wellSet = getWellSetForMultichannel(labwareName, commandWellName)
const wellSet = getWellSetForMultichannel(labwareType, commandWellName)
return Boolean(wellSet && wellSet.includes(wellName))
} else {
console.error(`Unexpected number of channels: ${channels || '?'}. Could not get tip highlight state`)
Expand Down Expand Up @@ -93,7 +94,9 @@ export const getTipsForCurrentStep: GetTipSelector = createSelector(
getInitialTips,
getLastValidTips,
getLabwareIdProp,
(orderedSteps, robotStateTimeline, hoveredStepId, activeItem, initialTips, lastValidTips, labwareId) => {
steplistSelectors.getHoveredSubstep,
getAllSubsteps,
(orderedSteps, robotStateTimeline, hoveredStepId, activeItem, initialTips, lastValidTips, labwareId, hoveredSubstepIdentifier, allSubsteps) => {
if (!activeItem.isStep) {
const terminalId = activeItem.id
if (terminalId === START_TERMINAL_ITEM_ID) {
Expand Down Expand Up @@ -128,9 +131,39 @@ export const getTipsForCurrentStep: GetTipSelector = createSelector(
: false

// show highlights of tips used by current frame, if user is hovering
const highlighted = (hovered && currentFrame)
? getTipHighlighted(labwareId, wellName, currentFrame)
: false
let highlighted = false
if (hoveredSubstepIdentifier && currentFrame) {
const {substepIndex} = hoveredSubstepIdentifier
const substepsForStep = allSubsteps[hoveredSubstepIdentifier.stepId]

if (substepsForStep && substepsForStep.stepType !== 'pause') {
if (substepsForStep.multichannel) {
const hoveredSubstepData = substepsForStep.multiRows[substepIndex][0] // just use first multi row

const labwareType = StepGeneration.getLabwareType(labwareId, currentFrame.robotState)
const wellSet = (labwareType && hoveredSubstepData.activeTips)
? getWellSetForMultichannel(labwareType, hoveredSubstepData.activeTips.well)
: []

highlighted = (hoveredSubstepData &&
hoveredSubstepData.activeTips &&
hoveredSubstepData.activeTips.labware === labwareId &&
Boolean(wellSet && wellSet.includes(wellName))
) || false
} else {
// single-channel
const hoveredSubstepData = substepsForStep.rows[substepIndex]

highlighted = (hoveredSubstepData &&
hoveredSubstepData.activeTips &&
hoveredSubstepData.activeTips.labware === labwareId &&
hoveredSubstepData.activeTips.well === wellName
) || false
}
}
} else if (hovered && currentFrame) {
highlighted = getTipHighlighted(labwareId, wellName, currentFrame)
}

return {
empty,
Expand Down