From a93f50d82daa0126bb90de1946b0d982a1ea09b7 Mon Sep 17 00:00:00 2001 From: IanLondon Date: Tue, 17 Sep 2019 13:12:35 -0400 Subject: [PATCH] feat(protocol-designer): avoid use of labware "format" closes #3894 --- .../components/labware/SelectableLabware.js | 7 +- protocol-designer/src/labware-defs/actions.js | 2 +- .../formLevel/handleFormChange/utils.js | 2 +- .../src/top-selectors/substep-highlight.js | 2 +- .../src/top-selectors/tip-contents/index.js | 15 ++- protocol-designer/src/ui/labware/selectors.js | 26 ++-- protocol-designer/src/utils/index.js | 7 ++ .../src/well-selection/test/utils.test.js | 108 ----------------- protocol-designer/src/well-selection/utils.js | 61 ---------- .../js/helpers/__tests__/wellSets.test.js | 114 ++++++++++++++++++ .../js/helpers/canPipetteUseLabware.js | 14 +-- shared-data/js/helpers/index.js | 4 +- shared-data/js/helpers/wellSets.js | 73 +++++++++++ 13 files changed, 224 insertions(+), 211 deletions(-) delete mode 100644 protocol-designer/src/well-selection/test/utils.test.js delete mode 100644 protocol-designer/src/well-selection/utils.js create mode 100644 shared-data/js/helpers/__tests__/wellSets.test.js create mode 100644 shared-data/js/helpers/wellSets.js diff --git a/protocol-designer/src/components/labware/SelectableLabware.js b/protocol-designer/src/components/labware/SelectableLabware.js index 26ea0475d28..b438da9f456 100644 --- a/protocol-designer/src/components/labware/SelectableLabware.js +++ b/protocol-designer/src/components/labware/SelectableLabware.js @@ -2,9 +2,12 @@ import * as React from 'react' import reduce from 'lodash/reduce' -import { getCollidingWells, arrayToWellGroup } from '../../utils' +import { + arrayToWellGroup, + getCollidingWells, + getWellSetForMultichannel, +} from '../../utils' import { SELECTABLE_WELL_CLASS } from '../../constants' -import { getWellSetForMultichannel } from '../../well-selection/utils' import SingleLabware from './SingleLabware' import SelectionRect from '../SelectionRect' import WellTooltip from './WellTooltip' diff --git a/protocol-designer/src/labware-defs/actions.js b/protocol-designer/src/labware-defs/actions.js index 400a052db72..7a98763a7c5 100644 --- a/protocol-designer/src/labware-defs/actions.js +++ b/protocol-designer/src/labware-defs/actions.js @@ -8,7 +8,7 @@ import uniqBy from 'lodash/uniqBy' import labwareSchema from '@opentrons/shared-data/labware/schemas/2.json' import { getLabwareDefURI } from '@opentrons/shared-data' import * as labwareDefSelectors from './selectors' -import { getAllWellSetsForLabware } from '../well-selection/utils' +import { getAllWellSetsForLabware } from '../utils' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { GetState, ThunkAction, ThunkDispatch } from '../types' import type { LabwareUploadMessage } from './types' diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.js b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.js index 3eac1d755d8..9af7fecbbc3 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.js +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.js @@ -3,8 +3,8 @@ import assert from 'assert' import round from 'lodash/round' import uniq from 'lodash/uniq' import { canPipetteUseLabware } from '@opentrons/shared-data' +import { getWellSetForMultichannel } from '../../../utils' import { getPipetteCapacity } from '../../../pipettes/pipetteData' -import { getWellSetForMultichannel } from '../../../well-selection/utils' import type { LabwareDefinition2, PipetteChannels, diff --git a/protocol-designer/src/top-selectors/substep-highlight.js b/protocol-designer/src/top-selectors/substep-highlight.js index 1222af04657..a44312587eb 100644 --- a/protocol-designer/src/top-selectors/substep-highlight.js +++ b/protocol-designer/src/top-selectors/substep-highlight.js @@ -1,6 +1,7 @@ // @flow import { createSelector } from 'reselect' import { getWellNamePerMultiTip } from '@opentrons/shared-data' +import { getWellSetForMultichannel } from '../utils' import type { Command } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import mapValues from 'lodash/mapValues' @@ -10,7 +11,6 @@ import * as StepGeneration from '../step-generation' import { selectors as stepFormSelectors } from '../step-forms' import { selectors as fileDataSelectors } from '../file-data' import { selectors as stepsSelectors } from '../ui/steps' -import { getWellSetForMultichannel } from '../well-selection/utils' import type { WellGroup } from '@opentrons/components' import type { Selector } from '../types' diff --git a/protocol-designer/src/top-selectors/tip-contents/index.js b/protocol-designer/src/top-selectors/tip-contents/index.js index bdf98a78f4e..36e54fdf76d 100644 --- a/protocol-designer/src/top-selectors/tip-contents/index.js +++ b/protocol-designer/src/top-selectors/tip-contents/index.js @@ -3,14 +3,14 @@ import { createSelector } from 'reselect' import noop from 'lodash/noop' import reduce from 'lodash/reduce' import mapValues from 'lodash/mapValues' -import type { WellGroup } from '@opentrons/components' +import { getWellSetForMultichannel } from '../../utils' import * as StepGeneration from '../../step-generation' import { allSubsteps as getAllSubsteps } from '../substeps' import { START_TERMINAL_ITEM_ID, END_TERMINAL_ITEM_ID } from '../../steplist' 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 { WellGroup } from '@opentrons/components' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { Command } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' import type { OutputSelector } from 'reselect' @@ -226,12 +226,11 @@ export const getTipsForCurrentStep: GetTipSelector = createSelector( const hoveredSubstepData = substepsForStep.multiRows[substepIndex][0] // just use first multi row - const wellSet = hoveredSubstepData.activeTips - ? getWellSetForMultichannel( - labwareDef, - hoveredSubstepData.activeTips.well - ) - : [] + let wellSet: ?Array = [] + const hoveredTipWell = hoveredSubstepData.activeTips?.well + if (hoveredTipWell != null) { + wellSet = getWellSetForMultichannel(labwareDef, hoveredTipWell) + } highlighted = (hoveredSubstepData && diff --git a/protocol-designer/src/ui/labware/selectors.js b/protocol-designer/src/ui/labware/selectors.js index e6d9d1f1cc8..d6083394225 100644 --- a/protocol-designer/src/ui/labware/selectors.js +++ b/protocol-designer/src/ui/labware/selectors.js @@ -2,11 +2,7 @@ import { createSelector } from 'reselect' import mapValues from 'lodash/mapValues' import reduce from 'lodash/reduce' -import { - getIsTiprack, - getLabwareDisplayName, - getLabwareFormat, -} from '@opentrons/shared-data' +import { getIsTiprack, getLabwareDisplayName } from '@opentrons/shared-data' import { selectors as stepFormSelectors } from '../../step-forms' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' @@ -57,16 +53,16 @@ export const getDisposalLabwareOptions: Selector = createSelector( reduce( labwareEntities, (acc: Options, labware: LabwareEntity, labwareId): Options => { - if (getLabwareFormat(labware.def) === 'trash') { - return [ - ...acc, - { - name: names[labwareId], - value: labwareId, - }, - ] - } - return acc + // TODO: Ian 2019-09-17 if we create a way to distinguish "intended for disposal" + // labware, use that here as a filter. + // Until then, allow all labware to be used for disposal + return [ + ...acc, + { + name: names[labwareId], + value: labwareId, + }, + ] }, [] ) diff --git a/protocol-designer/src/utils/index.js b/protocol-designer/src/utils/index.js index 5f05dc90e15..8e440e3f487 100644 --- a/protocol-designer/src/utils/index.js +++ b/protocol-designer/src/utils/index.js @@ -1,5 +1,6 @@ // @flow import uuidv1 from 'uuid/v1' +import { makeWellSetHelpers } from '@opentrons/shared-data' import type { WellGroup } from '@opentrons/components' import type { BoundingRect, GenericRect } from '../collision-types' @@ -70,3 +71,9 @@ export const getCollidingWells = ( // TODO IMMEDIATELY use where appropriate export const arrayToWellGroup = (w: Array): WellGroup => w.reduce((acc, wellName) => ({ ...acc, [wellName]: null }), {}) + +// cross-PD memoization of well set utils +export const { + getAllWellSetsForLabware, + getWellSetForMultichannel, +} = makeWellSetHelpers() diff --git a/protocol-designer/src/well-selection/test/utils.test.js b/protocol-designer/src/well-selection/test/utils.test.js deleted file mode 100644 index 7b86cc74808..00000000000 --- a/protocol-designer/src/well-selection/test/utils.test.js +++ /dev/null @@ -1,108 +0,0 @@ -import fixture_12_trough from '@opentrons/shared-data/labware/fixtures/2/fixture_12_trough.json' -import fixture_96_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json' -import fixture_384_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_384_plate.json' -import { getWellSetForMultichannel } from '../utils' - -describe('getWellSetForMultichannel (integration test)', () => { - test('96-flat', () => { - const labware = fixture_96_plate - expect(getWellSetForMultichannel(labware, 'A1')).toEqual([ - 'A1', - 'B1', - 'C1', - 'D1', - 'E1', - 'F1', - 'G1', - 'H1', - ]) - - expect(getWellSetForMultichannel(labware, 'B1')).toEqual([ - 'A1', - 'B1', - 'C1', - 'D1', - 'E1', - 'F1', - 'G1', - 'H1', - ]) - - expect(getWellSetForMultichannel(labware, 'H1')).toEqual([ - 'A1', - 'B1', - 'C1', - 'D1', - 'E1', - 'F1', - 'G1', - 'H1', - ]) - - expect(getWellSetForMultichannel(labware, 'A2')).toEqual([ - 'A2', - 'B2', - 'C2', - 'D2', - 'E2', - 'F2', - 'G2', - 'H2', - ]) - }) - - test('invalid well', () => { - const labware = fixture_96_plate - expect(getWellSetForMultichannel(labware, 'A13')).toBeFalsy() - }) - - test('trough-12row', () => { - const labware = fixture_12_trough - expect(getWellSetForMultichannel(labware, 'A1')).toEqual([ - 'A1', - 'A1', - 'A1', - 'A1', - 'A1', - 'A1', - 'A1', - 'A1', - ]) - - expect(getWellSetForMultichannel(labware, 'A2')).toEqual([ - 'A2', - 'A2', - 'A2', - 'A2', - 'A2', - 'A2', - 'A2', - 'A2', - ]) - }) - - test('384-plate', () => { - const labware = fixture_384_plate - expect(getWellSetForMultichannel(labware, 'C1')).toEqual([ - 'A1', - 'C1', - 'E1', - 'G1', - 'I1', - 'K1', - 'M1', - 'O1', - ]) - - expect(getWellSetForMultichannel(labware, 'F2')).toEqual([ - 'B2', - 'D2', - 'F2', - 'H2', - 'J2', - 'L2', - 'N2', - 'P2', - ]) - }) -}) diff --git a/protocol-designer/src/well-selection/utils.js b/protocol-designer/src/well-selection/utils.js deleted file mode 100644 index e6ed56759bf..00000000000 --- a/protocol-designer/src/well-selection/utils.js +++ /dev/null @@ -1,61 +0,0 @@ -// @flow -import { - getWellNamePerMultiTip, - getLabwareDefURI, -} from '@opentrons/shared-data' -import type { LabwareDefinition2 } from '@opentrons/shared-data' - -type WellSetByPrimaryWell = Array> - -/** Compute all well sets for a labware type. - * A well set is array of 8 wells that an 8 channel pipettes can fit into, - * eg ['A1', 'C1', 'E1', 'G1', 'I1', 'K1', 'M1', 'O1'] is a well set in a 384 plate. - **/ -function _getAllWellSetsForLabware( - labwareDef: LabwareDefinition2 -): WellSetByPrimaryWell { - const allWells: Array = Object.keys(labwareDef.wells) - return allWells.reduce( - (acc: WellSetByPrimaryWell, well: string): WellSetByPrimaryWell => { - const wellSet = getWellNamePerMultiTip(labwareDef, well) - return wellSet === null ? acc : [...acc, wellSet] - }, - [] - ) -} - -let cache: { - [labwareDefURI: string]: ?{ - labwareDef: LabwareDefinition2, - wellSetByPrimaryWell: WellSetByPrimaryWell, - }, -} = {} - -// memoized -export const getAllWellSetsForLabware = ( - labwareDef: LabwareDefinition2 -): WellSetByPrimaryWell => { - const labwareDefURI = getLabwareDefURI(labwareDef) - const c = cache[labwareDefURI] - // use cached version only if labwareDef is shallowly equal, in case - // custom labware defs are changed without giving them a new URI - if (c && c.labwareDef === labwareDef) { - return c.wellSetByPrimaryWell - } - const wellSetByPrimaryWell = _getAllWellSetsForLabware(labwareDef) - cache[labwareDefURI] = { labwareDef, wellSetByPrimaryWell } - return wellSetByPrimaryWell -} - -export function getWellSetForMultichannel( - labwareDef: LabwareDefinition2, - well: string -): ?Array { - /** Given a well for a labware, returns the well set it belongs to (or null) - * for 8-channel access. - * Ie: C2 for 96-flat => ['A2', 'B2', 'C2', ... 'H2'] - * Or A1 for trough => ['A1', 'A1', 'A1', ...] - **/ - const allWellSets = getAllWellSetsForLabware(labwareDef) - return allWellSets.find((wellSet: Array) => wellSet.includes(well)) -} diff --git a/shared-data/js/helpers/__tests__/wellSets.test.js b/shared-data/js/helpers/__tests__/wellSets.test.js new file mode 100644 index 00000000000..8a72fd6143a --- /dev/null +++ b/shared-data/js/helpers/__tests__/wellSets.test.js @@ -0,0 +1,114 @@ +// @flow +import fixture_12_trough from '../../../labware/fixtures/2/fixture_12_trough.json' +import fixture_96_plate from '../../../labware/fixtures/2/fixture_96_plate.json' +import fixture_384_plate from '../../../labware/fixtures/2/fixture_384_plate.json' +import { makeWellSetHelpers } from '../wellSets' + +describe('getWellSetForMultichannel (integration test)', () => { + let getWellSetForMultichannel + beforeEach(() => { + const helpers = makeWellSetHelpers() + getWellSetForMultichannel = helpers.getWellSetForMultichannel + }) + test('96-flat', () => { + const labwareDef = fixture_96_plate + expect(getWellSetForMultichannel(labwareDef, 'A1')).toEqual([ + 'A1', + 'B1', + 'C1', + 'D1', + 'E1', + 'F1', + 'G1', + 'H1', + ]) + + expect(getWellSetForMultichannel(labwareDef, 'B1')).toEqual([ + 'A1', + 'B1', + 'C1', + 'D1', + 'E1', + 'F1', + 'G1', + 'H1', + ]) + + expect(getWellSetForMultichannel(labwareDef, 'H1')).toEqual([ + 'A1', + 'B1', + 'C1', + 'D1', + 'E1', + 'F1', + 'G1', + 'H1', + ]) + + expect(getWellSetForMultichannel(labwareDef, 'A2')).toEqual([ + 'A2', + 'B2', + 'C2', + 'D2', + 'E2', + 'F2', + 'G2', + 'H2', + ]) + }) + + test('invalid well', () => { + const labwareDef = fixture_96_plate + expect(getWellSetForMultichannel(labwareDef, 'A13')).toBeFalsy() + }) + + test('trough-12row', () => { + const labwareDef = fixture_12_trough + expect(getWellSetForMultichannel(labwareDef, 'A1')).toEqual([ + 'A1', + 'A1', + 'A1', + 'A1', + 'A1', + 'A1', + 'A1', + 'A1', + ]) + + expect(getWellSetForMultichannel(labwareDef, 'A2')).toEqual([ + 'A2', + 'A2', + 'A2', + 'A2', + 'A2', + 'A2', + 'A2', + 'A2', + ]) + }) + + test('384-plate', () => { + const labwareDef = fixture_384_plate + expect(getWellSetForMultichannel(labwareDef, 'C1')).toEqual([ + 'A1', + 'C1', + 'E1', + 'G1', + 'I1', + 'K1', + 'M1', + 'O1', + ]) + + expect(getWellSetForMultichannel(labwareDef, 'F2')).toEqual([ + 'B2', + 'D2', + 'F2', + 'H2', + 'J2', + 'L2', + 'N2', + 'P2', + ]) + }) +}) diff --git a/shared-data/js/helpers/canPipetteUseLabware.js b/shared-data/js/helpers/canPipetteUseLabware.js index dc0165293a0..7d065131ac7 100644 --- a/shared-data/js/helpers/canPipetteUseLabware.js +++ b/shared-data/js/helpers/canPipetteUseLabware.js @@ -1,23 +1,15 @@ // @flow -import { getLabwareFormat } from './' import type { LabwareDefinition2 } from '../types' import type { PipetteNameSpecs } from '../pipettes' -const FORMAT_METADATA = { - '96Standard': { multichannelAccess: true }, - '384Standard': { multichannelAccess: true }, - trough: { multichannelAccess: true }, - irregular: { multichannelAccess: false }, - trash: { multichannelAccess: true }, -} - export const canPipetteUseLabware = ( pipetteSpec: PipetteNameSpecs, labwareDef: LabwareDefinition2 ): ?boolean => { if (pipetteSpec.channels === 1) { + // assume all labware can be used by single-channel return true } - const format = getLabwareFormat(labwareDef) - return FORMAT_METADATA[format].multichannelAccess + + return false // TODO IMMEDIATELY } diff --git a/shared-data/js/helpers/index.js b/shared-data/js/helpers/index.js index 97561e7c08c..cb8e3a9f95b 100644 --- a/shared-data/js/helpers/index.js +++ b/shared-data/js/helpers/index.js @@ -9,6 +9,7 @@ export { getWellNamePerMultiTip } from './getWellNamePerMultiTip' export { default as getWellTotalVolume } from './getWellTotalVolume' export { default as wellIsRect } from './wellIsRect' export * from './volume' +export * from './wellSets' export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean => def?.namespace === OPENTRONS_LABWARE_NAMESPACE @@ -41,9 +42,6 @@ export const getLabwareDisplayName = (labwareDef: LabwareDefinition2) => { return displayName } -export const getLabwareFormat = (labwareDef: LabwareDefinition2) => - labwareDef.parameters.format - export const getTiprackVolume = (labwareDef: LabwareDefinition2): number => { assert( labwareDef.parameters.isTiprack, diff --git a/shared-data/js/helpers/wellSets.js b/shared-data/js/helpers/wellSets.js new file mode 100644 index 00000000000..a4bf10af1bc --- /dev/null +++ b/shared-data/js/helpers/wellSets.js @@ -0,0 +1,73 @@ +// @flow +// A "well set" is array of wells corresponding to each tip of an 8 channel pipette. +// Eg ['A1', 'C1', 'E1', 'G1', 'I1', 'K1', 'M1', 'O1'] is a well set in a 384 plate. +// +// A trough-like well that encompasses all 8 tips at once has a well set +// ['A1', 'A1', 'A1', 'A1', 'A1', 'A1', 'A1', 'A1'] +// +// Well sets are determined by geometry. +// +// Labware with multiple positions for an 8-channel pipette have multiple well sets. +// For example, a 96 plate has 12 well sets, one for each column. +// A 384 plate has 48 well sets, 2 for each column b/c it has staggered columns. +// +// If a labware has no possible well sets, then it is not compatible with multi-channel pipettes. +import { getLabwareDefURI } from '@opentrons/shared-data' +import { getWellNamePerMultiTip } from './getWellNamePerMultiTip' +import type { LabwareDefinition2 } from '../types' + +type WellSetByPrimaryWell = Array> + +// Compute all well sets for a labware def (non-memoized) +export function _getAllWellSetsForLabware( + labwareDef: LabwareDefinition2 +): WellSetByPrimaryWell { + const allWells: Array = Object.keys(labwareDef.wells) + return allWells.reduce( + (acc: WellSetByPrimaryWell, well: string): WellSetByPrimaryWell => { + const wellSet = getWellNamePerMultiTip(labwareDef, well) + return wellSet === null ? acc : [...acc, wellSet] + }, + [] + ) +} + +// creates memoized getAllWellSetsForLabware + getWellSetForMultichannel fns. +export const makeWellSetHelpers = () => { + let cache: { + [labwareDefURI: string]: ?{| + labwareDef: LabwareDefinition2, + wellSetByPrimaryWell: WellSetByPrimaryWell, + |}, + } = {} + + const getAllWellSetsForLabware = ( + labwareDef: LabwareDefinition2 + ): WellSetByPrimaryWell => { + const labwareDefURI = getLabwareDefURI(labwareDef) + const c = cache[labwareDefURI] + // use cached version only if labwareDef is shallowly equal, in case + // custom labware defs are changed without giving them a new URI + if (c && c.labwareDef === labwareDef) { + return c.wellSetByPrimaryWell + } + const wellSetByPrimaryWell = _getAllWellSetsForLabware(labwareDef) + cache[labwareDefURI] = { labwareDef, wellSetByPrimaryWell } + return wellSetByPrimaryWell + } + + const getWellSetForMultichannel = ( + labwareDef: LabwareDefinition2, + well: string + ): ?Array => { + /** Given a well for a labware, returns the well set it belongs to (or null) + * for 8-channel access. + * Ie: C2 for 96-flat => ['A2', 'B2', 'C2', ... 'H2'] + * Or A1 for trough => ['A1', 'A1', 'A1', ...] + **/ + const allWellSets = getAllWellSetsForLabware(labwareDef) + return allWellSets.find((wellSet: Array) => wellSet.includes(well)) + } + + return { getAllWellSetsForLabware, getWellSetForMultichannel } +}