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): add load liquid commands #9923

Merged
merged 6 commits into from
Apr 13, 2022
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
2,695 changes: 2,695 additions & 0 deletions protocol-designer/fixtures/protocol/5/multipleLiquids.json

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions protocol-designer/src/file-data/__tests__/createFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
fixtureP300Single,
} from '@opentrons/shared-data/pipette/fixtures/name'
import { LabwareDefinition2 } from '@opentrons/shared-data'
import { getLoadLiquidCommands } from '../../load-file/migration/utils/getLoadLiquidCommands'
import { createFile, getLabwareDefinitionsInUse } from '../selectors'
import {
fileMetadata,
Expand All @@ -29,6 +30,12 @@ import {
} from '../../../../step-generation/src/types'
import { LabwareDefByDefURI } from '../../labware-defs'

jest.mock('../../load-file/migration/utils/getLoadLiquidCommands')

const mockGetLoadLiquidCommands = getLoadLiquidCommands as jest.MockedFunction<
typeof getLoadLiquidCommands
>

const getAjvValidator = (_protocolSchema: Record<string, any>) => {
const ajv = new Ajv({
allErrors: true,
Expand Down Expand Up @@ -57,6 +64,12 @@ const expectResultToMatchSchema = (
}

describe('createFile selector', () => {
beforeEach(() => {
mockGetLoadLiquidCommands.mockReturnValue([])
})
afterEach(() => {
jest.restoreAllMocks()
})
it('should return a schema-valid JSON V6 protocol', () => {
// @ts-expect-error(sa, 2021-6-15): resultFunc not part of Selector type
const result = createFile.resultFunc(
Expand All @@ -79,6 +92,10 @@ describe('createFile selector', () => {
// have the opportunity to validate their part of the schema
expect(!isEmpty(result.labware)).toBe(true)
expect(!isEmpty(result.pipettes)).toBe(true)
expect(mockGetLoadLiquidCommands).toHaveBeenCalledWith(
ingredients,
ingredLocations
)
})
})
describe('getLabwareDefinitionsInUse util', () => {
Expand Down
80 changes: 44 additions & 36 deletions protocol-designer/src/file-data/selectors/fileCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { uuid } from '../../utils'
import { selectors as ingredSelectors } from '../../labware-ingred/selectors'
import { selectors as stepFormSelectors } from '../../step-forms'
import { selectors as uiLabwareSelectors } from '../../ui/labware'
import { getLoadLiquidCommands } from '../../load-file/migration/utils/getLoadLiquidCommands'
import {
DEFAULT_MM_FROM_BOTTOM_ASPIRATE,
DEFAULT_MM_FROM_BOTTOM_DISPENSE,
Expand All @@ -32,8 +33,8 @@ import type {
CreateCommand,
ProtocolFile,
} from '@opentrons/shared-data/protocol/types/schemaV6'
import { Selector } from '../../types'
import {
import type { Selector } from '../../types'
import type {
LoadLabwareCreateCommand,
LoadModuleCreateCommand,
LoadPipetteCreateCommand,
Expand Down Expand Up @@ -108,6 +109,39 @@ export const createFile: Selector<ProtocolFile> = createSelector(
const { author, description, created } = fileMetadata
const name = fileMetadata.protocolName || 'untitled'
const lastModified = fileMetadata.lastModified
// TODO: Ian 2018-07-10 allow user to save steps in JSON file, even if those
// step never have saved forms.
// (We could just export the `steps` reducer, but we've sunset it)
const savedOrderedStepIds = orderedStepIds.filter(
stepId => savedStepForms[stepId]
)
const 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: typeof pipetteEntities[keyof typeof pipetteEntities]
): string | null | undefined => p.tiprackDefURI
),
dismissedWarnings,
ingredients,
ingredLocations,
savedStepForms,
orderedStepIds: savedOrderedStepIds,
},
}

const pipettes: ProtocolFile['pipettes'] = mapValues(
initialRobotState.pipettes,
Expand Down Expand Up @@ -179,6 +213,11 @@ export const createFile: Selector<ProtocolFile> = createSelector(
return loadLabwareCommand
}
)

const loadLiquidCommands = getLoadLiquidCommands(
ingredients,
ingredLocations
)
const modules: ProtocolFile['modules'] = mapValues(
moduleEntities,
(moduleEntity: ModuleEntity, moduleId: string) => ({
Expand All @@ -203,12 +242,7 @@ export const createFile: Selector<ProtocolFile> = createSelector(
return loadModuleCommand
}
)
// TODO: Ian 2018-07-10 allow user to save steps in JSON file, even if those
// step never have saved forms.
// (We could just export the `steps` reducer, but we've sunset it)
const savedOrderedStepIds = orderedStepIds.filter(
stepId => savedStepForms[stepId]
)

const labwareDefinitions = getLabwareDefinitionsInUse(
labwareEntities,
pipetteEntities,
Expand All @@ -218,7 +252,7 @@ export const createFile: Selector<ProtocolFile> = createSelector(
...loadPipetteCommands,
...loadLabwareCommands,
...loadModuleCommands,
// TODO: generate load liquid commands https://github.com/Opentrons/opentrons/issues/9702
...loadLiquidCommands,
]

const nonLoadCommands: CreateCommand[] = flatMap(
Expand All @@ -240,33 +274,7 @@ export const createFile: Selector<ProtocolFile> = createSelector(
subcategory: null,
tags: [],
},
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: typeof pipetteEntities[keyof typeof pipetteEntities]
): string | null | undefined => p.tiprackDefURI
),
dismissedWarnings,
ingredients,
ingredLocations,
savedStepForms,
orderedStepIds: savedOrderedStepIds,
},
},
designerApplication,
robot: {
model: OT2_STANDARD_MODEL,
deckId: OT2_STANDARD_DECKID,
Expand Down
22 changes: 8 additions & 14 deletions protocol-designer/src/load-file/migration/6_0_0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { uuid } from '../../utils'
// and labware access parameters, renames AirGap to aspirate, and removes all temporal properties from labware, pipettes,
// and module keys such as slot, mount
// and renames well to wellName
import { getLoadLiquidCommands } from './utils/getLoadLiquidCommands'
import type {
LoadPipetteCreateCommand,
LoadModuleCreateCommand,
Expand All @@ -21,18 +22,7 @@ import type {
CreateCommand,
ProtocolFile,
} from '@opentrons/shared-data/protocol/types/schemaV6'

interface DesignerApplicationData {
ingredients: Record<
string,
{
name: string
description: string | null
serialize: boolean
liquidGroupId: string
}
>
}
import type { DesignerApplicationData } from './utils/getLoadLiquidCommands'

const PD_VERSION = '6.0.0'
const SCHEMA_VERSION = 6
Expand Down Expand Up @@ -85,7 +75,7 @@ const migrateCommands = (
export const migrateFile = (
appData: ProtocolFileV5<DesignerApplicationData>
): ProtocolFile => {
const { pipettes, labware, modules, commands } = appData
const { pipettes, labware, modules, commands, designerApplication } = appData
const loadPipetteCommands: LoadPipetteCreateCommand[] = map(
pipettes,
(pipette, pipetteId) => {
Expand Down Expand Up @@ -131,6 +121,10 @@ export const migrateFile = (
}
)

const loadLiquidCommands = getLoadLiquidCommands(
designerApplication?.data?.ingredients,
designerApplication?.data?.ingredLocations
)
const migratedV5Commands = migrateCommands(commands)

const liquids: ProtocolFile['liquids'] =
Expand Down Expand Up @@ -168,7 +162,7 @@ export const migrateFile = (
modules: migrateModules(appData.modules),
liquids,
commands: [
// TODO: generate load liquid commands https://github.com/Opentrons/opentrons/issues/9702
...loadLiquidCommands,
...loadPipetteCommands,
...loadModuleCommands,
...loadLabwareCommands,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { migrateFile } from '../6_0_0'
import { getLoadLiquidCommands } from '../utils/getLoadLiquidCommands'
import _oldProtocol from 'protocol-designer/fixtures/protocol/5/doItAllV5.json'
import type { ProtocolFile, ProtocolFileV5 } from '@opentrons/shared-data'
import type { ProtocolFileV5 } from '@opentrons/shared-data'

const oldProtocol = (_oldProtocol as unknown) as ProtocolFileV5<any>

jest.mock('../utils/getLoadLiquidCommands')

const mockGetLoadLiquidCommands = getLoadLiquidCommands as jest.MockedFunction<
typeof getLoadLiquidCommands
>

describe('v6 migration', () => {
let migratedFile = {} as ProtocolFile
beforeEach(() => {
migratedFile = migrateFile(oldProtocol)
mockGetLoadLiquidCommands.mockReturnValue([])
})
afterEach(() => {
jest.restoreAllMocks()
})
it('removes slot from modules and labware', () => {
const migratedFile = migrateFile(oldProtocol)

expect(
oldProtocol.modules[
'0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType'
Expand All @@ -34,6 +45,7 @@ describe('v6 migration', () => {
).toBeUndefined()
})
it('removes mount from pipettes', () => {
const migratedFile = migrateFile(oldProtocol)
expect(
oldProtocol.pipettes['0b3f2210-75c7-11ea-b42f-4b64e50f43e5'].mount
).toEqual('left')
Expand All @@ -43,17 +55,20 @@ describe('v6 migration', () => {
).toBeUndefined()
})
it('adds deckId to Robot', () => {
const migratedFile = migrateFile(oldProtocol)
expect(oldProtocol.robot).toEqual({ model: 'OT-2 Standard' })
expect(migratedFile.robot).toEqual({
model: 'OT-2 Standard',
deckId: 'ot2_standard',
})
})
it('adds a liquids key', () => {
const migratedFile = migrateFile(oldProtocol)
const expectedLiquids = { '0': { displayName: 'Water', description: null } }
expect(migratedFile.liquids).toEqual(expectedLiquids)
})
it('creates loadModule commands', () => {
const migratedFile = migrateFile(oldProtocol)
const expectedLoadModuleCommands = [
{
key: expect.any(String),
Expand All @@ -80,6 +95,7 @@ describe('v6 migration', () => {
expect(loadModuleCommands).toEqual(expectedLoadModuleCommands)
})
it('creates loadPipette commands', () => {
const migratedFile = migrateFile(oldProtocol)
const expectedLoadPipetteCommands = [
{
key: expect.any(String),
Expand All @@ -96,6 +112,7 @@ describe('v6 migration', () => {
expect(loadPipetteCommands).toEqual(expectedLoadPipetteCommands)
})
it('creates loadLabware commands', () => {
const migratedFile = migrateFile(oldProtocol)
const loadLabwareCommands = migratedFile.commands.filter(
command => command.commandType === 'loadLabware'
)
Expand Down Expand Up @@ -140,7 +157,15 @@ describe('v6 migration', () => {
]
expect(loadLabwareCommands).toEqual(expectedLoadLabwareCommaands)
})
it('creates loadLiquid commands', () => {
migrateFile(oldProtocol)
expect(mockGetLoadLiquidCommands).toHaveBeenCalledWith(
_oldProtocol.designerApplication.data.ingredients,
_oldProtocol.designerApplication.data.ingredLocations
)
})
it('replaces air gap commands with aspirate commands', () => {
const migratedFile = migrateFile(oldProtocol)
const expectedConvertedAirgapCommands = [
{
key: expect.any(String), // no key used to exist in v5 commands
Expand Down
Loading