Skip to content

Commit

Permalink
feat(protocol-designer): loadAdapter and loadLabware python file (#17495
Browse files Browse the repository at this point in the history
)

partially addresses AUTH-1092

This pr creates the `load_adapter` and `load_labware` api commands in
the python export file. This also fixes a bug with generating the
`pythonNames` for adapters in the `createContainer` redux action where
the `displayCategory` for the adapter was incorrectly the
`displayCategory` for the labware
  • Loading branch information
jerader authored Feb 11, 2025
1 parent 875346d commit 024b4e9
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 35 deletions.
5 changes: 4 additions & 1 deletion protocol-designer/src/file-data/__tests__/createFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ requirements = {
}
def run(protocol: protocol_api.ProtocolContext):
pass
# Load Labware:
mockPythonName = protocol.load_labware("fixture_trash", "12")
mockPythonName = protocol.load_labware("fixture_tiprack_10_ul", "1")
mockPythonName = protocol.load_labware("fixture_96_plate", "7")
`.trimStart()
)
})
Expand Down
141 changes: 115 additions & 26 deletions protocol-designer/src/file-data/__tests__/pythonFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import {
MAGNETIC_BLOCK_TYPE,
MAGNETIC_BLOCK_V1,
OT2_ROBOT_TYPE,
fixture96Plate,
fixtureTiprackAdapter,
} from '@opentrons/shared-data'
import {
getLoadAdapters,
getLoadLabware,
getLoadModules,
pythonMetadata,
pythonRequirements,
} from '../selectors/pythonFile'
import type { TimelineFrame } from '@opentrons/step-generation'
import type { LabwareDefinition2 } from '@opentrons/shared-data'
import type { LabwareEntities, TimelineFrame } from '@opentrons/step-generation'
import type { ModuleEntities } from '../../step-forms'

describe('pythonMetadata', () => {
Expand Down Expand Up @@ -64,42 +69,126 @@ requirements = {
})
})

const moduleId = '1'
const moduleId2 = '2'
const moduleId3 = '3'
const mockModuleEntities: ModuleEntities = {
[moduleId]: {
id: moduleId,
model: MAGNETIC_BLOCK_V1,
type: MAGNETIC_BLOCK_TYPE,
pythonName: 'magnetic_block_1',
},
[moduleId2]: {
id: moduleId2,
model: HEATERSHAKER_MODULE_V1,
type: HEATERSHAKER_MODULE_TYPE,
pythonName: 'heater_shaker_1',
},
[moduleId3]: {
id: moduleId3,
model: MAGNETIC_BLOCK_V1,
type: MAGNETIC_BLOCK_TYPE,
pythonName: 'magnetic_block_2',
},
}
const labwareId1 = 'labwareId1'
const labwareId2 = 'labwareId2'
const labwareId3 = 'labwareId3'
const labwareId4 = 'labwareId4'
const labwareId5 = 'labwareId5'

const mockLabwareEntities: LabwareEntities = {
[labwareId1]: {
id: labwareId1,
labwareDefURI: 'fixture/fixture_flex_96_tiprack_adapter/1',
def: fixtureTiprackAdapter as LabwareDefinition2,
pythonName: 'adapter_1',
},
[labwareId2]: {
id: labwareId2,
labwareDefURI: 'fixture/fixture_flex_96_tiprack_adapter/1',
def: fixtureTiprackAdapter as LabwareDefinition2,
pythonName: 'adapter_2',
},
[labwareId3]: {
id: labwareId3,
labwareDefURI: 'fixture/fixture_96_plate/1',
def: fixture96Plate as LabwareDefinition2,
pythonName: 'well_plate_1',
},
[labwareId4]: {
id: labwareId4,
labwareDefURI: 'fixture/fixture_96_plate/1',
def: fixture96Plate as LabwareDefinition2,
pythonName: 'well_plate_2',
},
[labwareId5]: {
id: labwareId5,
labwareDefURI: 'fixture/fixture_96_plate/1',
def: fixture96Plate as LabwareDefinition2,
pythonName: 'well_plate_3',
},
}

const labwareRobotState: TimelineFrame['labware'] = {
// adapter on a module
[labwareId1]: { slot: moduleId },
// adapter on a slot
[labwareId2]: { slot: 'B2' },
// labware on an adapter on a slot
[labwareId3]: { slot: labwareId2 },
// labware on a module
[labwareId4]: { slot: moduleId3 },
// labware on a slot
[labwareId5]: { slot: 'C2' },
}

describe('getLoadModules', () => {
it('should generate loadModules', () => {
const moduleId = '1'
const moduleId2 = '2'
const moduleId3 = '3'
const mockModuleEntities: ModuleEntities = {
[moduleId]: {
id: moduleId,
model: MAGNETIC_BLOCK_V1,
type: MAGNETIC_BLOCK_TYPE,
pythonName: 'magnetic_block_1',
},
[moduleId2]: {
id: moduleId2,
model: HEATERSHAKER_MODULE_V1,
type: HEATERSHAKER_MODULE_TYPE,
pythonName: 'heater_shaker_1',
},
[moduleId3]: {
id: moduleId3,
model: MAGNETIC_BLOCK_V1,
type: MAGNETIC_BLOCK_TYPE,
pythonName: 'magnetic_block_2',
},
}
const modules: TimelineFrame['modules'] = {
[moduleId]: { slot: 'B1', moduleState: {} as any },
[moduleId2]: { slot: 'A1', moduleState: {} as any },
[moduleId3]: { slot: 'A2', moduleState: {} as any },
}

expect(getLoadModules(mockModuleEntities, modules)).toBe(
`# Load Modules:
`
# Load Modules:
magnetic_block_1 = protocol.load_module("magneticBlockV1", "B1")
heater_shaker_1 = protocol.load_module("heaterShakerModuleV1", "A1")
magnetic_block_2 = protocol.load_module("magneticBlockV1", "A2")`
magnetic_block_2 = protocol.load_module("magneticBlockV1", "A2")`.trimStart()
)
})
})

describe('getLoadAdapters', () => {
it('should generate loadAdapters for 2 adapters', () => {
expect(
getLoadAdapters(
mockModuleEntities,
mockLabwareEntities,
labwareRobotState
)
).toBe(
`
# Load Adapters:
adapter_1 = magnetic_block_1.load_adapter("fixture_flex_96_tiprack_adapter")
adapter_2 = protocol.load_adapter("fixture_flex_96_tiprack_adapter", "B2")`.trimStart()
)
})
})

describe('getLoadLabware', () => {
it('should generate loadLabware for 3 labware', () => {
expect(
getLoadLabware(mockModuleEntities, mockLabwareEntities, labwareRobotState)
).toBe(
`
# Load Labware:
well_plate_1 = adapter_2.load_labware("fixture_96_plate")
well_plate_2 = magnetic_block_2.load_labware("fixture_96_plate")
well_plate_3 = protocol.load_labware("fixture_96_plate", "C2")`.trimStart()
)
})
})
69 changes: 65 additions & 4 deletions protocol-designer/src/file-data/selectors/pythonFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@opentrons/step-generation'
import type {
InvariantContext,
LabwareEntities,
ModuleEntities,
TimelineFrame,
} from '@opentrons/step-generation'
Expand Down Expand Up @@ -77,17 +78,77 @@ export function getLoadModules(
return hasModules ? `# Load Modules:\n${pythonModules}` : ''
}

export function getLoadAdapters(
moduleEntities: ModuleEntities,
labwareEntities: LabwareEntities,
labwareRobotState: TimelineFrame['labware']
): string {
const adapterEntities = Object.values(labwareEntities).filter(lw =>
lw.def.allowedRoles?.includes('adapter')
)
const pythonAdapters = Object.values(adapterEntities)
.map(adapter => {
const adapterSlot = labwareRobotState[adapter.id].slot
const onModule = moduleEntities[adapterSlot] != null
const location = onModule
? moduleEntities[adapterSlot].pythonName
: PROTOCOL_CONTEXT_NAME
const slotInfo = onModule ? '' : `, ${formatPyStr(adapterSlot)}`

return `${adapter.pythonName} = ${location}.load_adapter(${formatPyStr(
adapter.def.parameters.loadName
)}${slotInfo})`
})
.join('\n')

return pythonAdapters ? `# Load Adapters:\n${pythonAdapters}` : ''
}

export function getLoadLabware(
moduleEntities: ModuleEntities,
allLabwareEntities: LabwareEntities,
labwareRobotState: TimelineFrame['labware']
): string {
const labwareEntities = Object.values(allLabwareEntities).filter(
lw => !lw.def.allowedRoles?.includes('adapter')
)
const pythonLabware = Object.values(labwareEntities)
.map(labware => {
const labwareSlot = labwareRobotState[labware.id].slot
const onModule = moduleEntities[labwareSlot] != null
const onAdapter = allLabwareEntities[labwareSlot] != null
let location = PROTOCOL_CONTEXT_NAME
if (onAdapter) {
location = allLabwareEntities[labwareSlot].pythonName
} else if (onModule) {
location = moduleEntities[labwareSlot].pythonName
}
const slotInfo =
onModule || onAdapter ? '' : `, ${formatPyStr(labwareSlot)}`

return `${labware.pythonName} = ${location}.load_labware(${formatPyStr(
labware.def.parameters.loadName
)}${slotInfo})`
})
.join('\n')

return pythonLabware ? `# Load Labware:\n${pythonLabware}` : ''
}

export function pythonDefRun(
invariantContext: InvariantContext,
robotState: TimelineFrame
): string {
const { moduleEntities } = invariantContext

const loadModules = getLoadModules(moduleEntities, robotState.modules)
const { moduleEntities, labwareEntities } = invariantContext
const { modules, labware } = robotState
const loadModules = getLoadModules(moduleEntities, modules)
const loadAdapters = getLoadAdapters(moduleEntities, labwareEntities, labware)
const loadLabware = getLoadLabware(moduleEntities, labwareEntities, labware)

const sections: string[] = [
loadModules,
// loadLabware(),
loadAdapters,
loadLabware,
// loadInstruments(),
// defineLiquids(),
// loadLiquids(),
Expand Down
11 changes: 7 additions & 4 deletions protocol-designer/src/labware-ingred/actions/thunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const createContainer: (
const labwareDef = labwareDefSelectors.getLabwareDefsByURI(state)[
args.labwareDefURI
]
const displayCategory = labwareDef.metadata.displayCategory
const labwareDisplayCategory = labwareDef.metadata.displayCategory
const slot =
args.slot ||
getNextAvailableDeckSlot(initialDeckSetup, robotType, labwareDef)
Expand All @@ -90,14 +90,17 @@ export const createContainer: (
: null

if (adapterId != null && args.adapterUnderLabwareDefURI != null) {
const adapterDef = labwareDefSelectors.getLabwareDefsByURI(state)[
args.adapterUnderLabwareDefURI
]
dispatch({
type: 'CREATE_CONTAINER',
payload: {
...args,
labwareDefURI: args.adapterUnderLabwareDefURI,
id: adapterId,
slot,
displayCategory,
displayCategory: adapterDef.metadata.displayCategory,
},
})
dispatch({
Expand All @@ -106,13 +109,13 @@ export const createContainer: (
...args,
id,
slot: adapterId,
displayCategory,
displayCategory: labwareDisplayCategory,
},
})
} else {
dispatch({
type: 'CREATE_CONTAINER',
payload: { ...args, id, slot, displayCategory },
payload: { ...args, id, slot, displayCategory: labwareDisplayCategory },
})
}
if (isTiprack) {
Expand Down

0 comments on commit 024b4e9

Please sign in to comment.