From 9176c7048497d5080f2a23ff25e225d6943c0d88 Mon Sep 17 00:00:00 2001 From: Jethary Date: Fri, 14 Nov 2025 12:39:34 -0800 Subject: [PATCH 1/5] refactor(app): clean up invariantContext construct for PV BREAKING CHANGE: closes AUTH-2486 closes aUTH-2486 --- .../VisualizerContainer.tsx | 8 +- .../constructInvariantContextFromAnalysis.ts | 209 ++++++++++++++++ ...onstructInvariantContextFromRunCommands.ts | 235 ------------------ step-generation/src/utils/index.ts | 3 +- 4 files changed, 214 insertions(+), 241 deletions(-) create mode 100644 step-generation/src/utils/constructInvariantContextFromAnalysis.ts delete mode 100644 step-generation/src/utils/constructInvariantContextFromRunCommands.ts diff --git a/app/src/pages/Desktop/Protocols/ProtocolVisualization/VisualizerContainer.tsx b/app/src/pages/Desktop/Protocols/ProtocolVisualization/VisualizerContainer.tsx index 0ee3b289613..e7f86b3265d 100644 --- a/app/src/pages/Desktop/Protocols/ProtocolVisualization/VisualizerContainer.tsx +++ b/app/src/pages/Desktop/Protocols/ProtocolVisualization/VisualizerContainer.tsx @@ -6,7 +6,7 @@ import { THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' import { - constructInvariantContextFromRunCommands, + constructInvariantContextFromAnalysis, getResultingTimelineFrameFromRunCommands, } from '@opentrons/step-generation' @@ -84,11 +84,11 @@ export function VisualizerContainer( ) const currentCommandsSlice = commands.slice(0, selectedCommandIndex + 1) - const invariantContextFromRunCommands = - constructInvariantContextFromRunCommands(commands) + const invariantContextFromAnalysis = + constructInvariantContextFromAnalysis(analysis) const { frame, invariantContext } = getResultingTimelineFrameFromRunCommands( currentCommandsSlice, - invariantContextFromRunCommands + invariantContextFromAnalysis ) const handlePlayPause = (): void => { diff --git a/step-generation/src/utils/constructInvariantContextFromAnalysis.ts b/step-generation/src/utils/constructInvariantContextFromAnalysis.ts new file mode 100644 index 00000000000..8b52ad47e2a --- /dev/null +++ b/step-generation/src/utils/constructInvariantContextFromAnalysis.ts @@ -0,0 +1,209 @@ +import { + getLabwareDefinitionsByURIForProtocol, + getModuleType, + getPipetteSpecsV2, +} from '@opentrons/shared-data' + +import { uuid } from '.' +import { GRIPPER_LOCATION } from '../constants' +import { createStagingAreaForInvariantContext } from './misc' + +import type { + PickUpTipRunTimeCommand, + ProtocolAnalysisOutput, + RunTimeCommand, +} from '@opentrons/shared-data' +import type { + InvariantContext, + LabwareEntities, + ModuleEntities, + PipetteEntities, + StagingAreaEntities, + TrashBinEntities, + WasteChuteEntities, +} from '../types' + +export function constructInvariantContextFromAnalysis( + analysis: ProtocolAnalysisOutput +): InvariantContext { + const { labware, modules, pipettes, commands } = analysis + const labwareDefinitions = getLabwareDefinitionsByURIForProtocol(commands) + + const moduleEntities: ModuleEntities = modules.reduce( + (acc: ModuleEntities, module) => { + const { id, model } = module + + acc[id] = { + id, + type: getModuleType(model), + model, + pythonName: 'n/a', + } + + return acc + }, + {} + ) + + const labwareEntities: LabwareEntities = labware.reduce( + (acc: LabwareEntities, loadedLabware) => { + const { id, definitionUri } = loadedLabware + const def = labwareDefinitions[definitionUri] + + if (def.schemaVersion === 3) { + return acc + } + + acc[id] = { + id, + labwareDefURI: definitionUri, + def, + pythonName: 'n/a', + } + + return acc + }, + {} + ) + + const pipetteEntities: PipetteEntities = pipettes.reduce( + (acc: PipetteEntities, pipette) => { + const { id, pipetteName } = pipette + const spec = getPipetteSpecsV2(pipetteName) + const tiprackIdsAssosciatedWithPipette = commands.filter( + (command): command is PickUpTipRunTimeCommand => + command.commandType === 'pickUpTip' && command.params.pipetteId === id + ) + const matchingLabwareEntities = tiprackIdsAssosciatedWithPipette.map( + pickUpTipCommand => labwareEntities[pickUpTipCommand.params.labwareId] + ) + const tiprackDefURIs = Array.from( + new Set(matchingLabwareEntities.map(entity => entity.labwareDefURI)) + ) + const tiprackLabwareDefs = Array.from( + new Set(matchingLabwareEntities.map(entity => entity.def)) + ) + if (spec == null) { + return acc + } + + acc[id] = { + name: pipetteName, + id, + tiprackLabwareDef: tiprackLabwareDefs, + tiprackDefURI: tiprackDefURIs, + spec, + pythonName: 'n/a', + } + + return acc + }, + {} + ) + const otherEntities = commands.reduce( + ( + acc: Omit< + InvariantContext, + 'labwareEntities' | 'moduleEntities' | 'pipetteEntities' + >, + command: RunTimeCommand + ) => { + if (command.commandType === 'loadLidStack' && command.result != null) { + const { params } = command + const newStagingAreaEntities: StagingAreaEntities = + createStagingAreaForInvariantContext(params) + + return { + ...acc, + + stagingAreaEntities: { + ...acc.stagingAreaEntities, + ...newStagingAreaEntities, + }, + } + } else if ( + (command.commandType === 'loadLabware' || + command.commandType === 'loadLid') && + command.result != null + ) { + const { params } = command + + const newStagingAreaEntities: StagingAreaEntities = + createStagingAreaForInvariantContext(params) + + return { + ...acc, + + stagingAreaEntities: { + ...acc.stagingAreaEntities, + ...newStagingAreaEntities, + }, + } + } else if ( + command.commandType === 'moveToAddressableArea' || + command.commandType === 'moveToAddressableAreaForDropTip' + ) { + const addressableAreaName = command.params.addressableAreaName + const id = `${uuid()}:${addressableAreaName}` + let location: string = GRIPPER_LOCATION + if (addressableAreaName === 'fixedTrash') { + location = 'cutout12' + } else if (addressableAreaName.includes('WasteChute')) { + location = 'cutoutD3' + } else if (addressableAreaName.includes('movableTrash')) { + location = `cutout${addressableAreaName.split('movableTrash')[1]}` + } + let trashBinEntities: TrashBinEntities = acc.trashBinEntities + if ( + !Object.values(acc.trashBinEntities).some( + entity => entity.location === location + ) && + addressableAreaName.includes('movableTrash') + ) { + trashBinEntities = { + ...acc.trashBinEntities, + [id]: { + pythonName: 'trash_bin_1', + id, + location, + }, + } + } + let wasteChuteEntities: WasteChuteEntities = acc.wasteChuteEntities + if (addressableAreaName.includes('WasteChute')) { + wasteChuteEntities = { + [id]: { + pythonName: 'waste_chute', + id, + location, + }, + } + } + return { + ...acc, + trashBinEntities, + wasteChuteEntities, + } + } + + return acc + }, + { + wasteChuteEntities: {}, + trashBinEntities: {}, + stagingAreaEntities: {}, + // the timeline scrubber doesn't visualize gripper right now + gripperEntities: {}, + // this util is used for the timeline scrubber. It grabs liquid info from analysis + // so this will not be wired up right now + liquidEntities: {}, + config: { OT_PD_DISABLE_MODULE_RESTRICTIONS: true }, + } + ) + return { + labwareEntities, + pipetteEntities, + moduleEntities, + ...otherEntities, + } +} diff --git a/step-generation/src/utils/constructInvariantContextFromRunCommands.ts b/step-generation/src/utils/constructInvariantContextFromRunCommands.ts deleted file mode 100644 index 84c3c8e0ceb..00000000000 --- a/step-generation/src/utils/constructInvariantContextFromRunCommands.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { - getLabwareDefURI, - getModuleType, - getPipetteSpecsV2, -} from '@opentrons/shared-data' - -import { uuid } from '.' -import { GRIPPER_LOCATION } from '../constants' -import { createStagingAreaForInvariantContext } from './misc' - -import type { - LoadLabwareRunTimeCommand, - PickUpTipRunTimeCommand, - RunTimeCommand, -} from '@opentrons/shared-data' -import type { - InvariantContext, - LabwareEntities, - ModuleEntities, - PipetteEntities, - StagingAreaEntities, - TrashBinEntities, - WasteChuteEntities, -} from '../types' - -export function constructInvariantContextFromRunCommands( - commands: RunTimeCommand[] -): InvariantContext { - return commands.reduce( - (acc: InvariantContext, command: RunTimeCommand) => { - if (command.commandType === 'loadLidStack' && command.result != null) { - const { result, params } = command - const amount = params.quantity - - const newStagingAreaEntities: StagingAreaEntities = - createStagingAreaForInvariantContext(params) - const newLabwareEntities: LabwareEntities = - // loadLabware commands from the backend can have schema 3 labware definitions. - // step-generation, and this function by extension, are not prepared to handle - // schema 3 yet. Just ignore those definitions for now. - // See also the loadPipette handling, below. - result.definition != null && result.definition.schemaVersion === 2 - ? (() => { - const def = result.definition - const labwareDefURI = getLabwareDefURI(def) - return result.labwareIds.slice(0, amount).reduce( - (entities: LabwareEntities, labwareId) => ({ - ...entities, - [labwareId]: { - id: labwareId, - labwareDefURI, - def, - pythonName: 'n/a', - }, - }), - {} - ) - })() - : {} - - return { - ...acc, - labwareEntities: { - ...acc.labwareEntities, - ...newLabwareEntities, - }, - stagingAreaEntities: { - ...acc.stagingAreaEntities, - ...newStagingAreaEntities, - }, - } - } else if ( - (command.commandType === 'loadLabware' || - command.commandType === 'loadLid') && - command.result != null - ) { - const { result, params } = command - - const newStagingAreaEntities: StagingAreaEntities = - createStagingAreaForInvariantContext(params) - const newLabwareEntities: LabwareEntities = - // todo(mm, 2025-05-16): - // loadLabware commands from the backend can have schema 3 labware definitions. - // step-generation, and this function by extension, are not prepared to handle - // schema 3 yet. Just ignore those definitions for now. - // See also the loadPipette handling, below. - result.definition != null && result.definition.schemaVersion === 2 - ? { - [result.labwareId]: { - id: result.labwareId, - labwareDefURI: getLabwareDefURI(result.definition), - def: result.definition, - // ProtocolTimelineScrubber won't need access to pythonNames - pythonName: 'n/a', - }, - } - : {} - - return { - ...acc, - labwareEntities: { - ...acc.labwareEntities, - ...newLabwareEntities, - }, - stagingAreaEntities: { - ...acc.stagingAreaEntities, - ...newStagingAreaEntities, - }, - } - } else if ( - command.commandType === 'loadModule' && - command.result != null - ) { - const result = command.result - const moduleEntities: ModuleEntities = { - ...acc.moduleEntities, - [result.moduleId]: { - id: result.moduleId, - type: getModuleType(command.params.model), - model: command.params.model, - pythonName: 'n/a', - }, - } - return { - ...acc, - moduleEntities, - } - } else if ( - command.commandType === 'loadPipette' && - command.result != null - ) { - const result = command.result - const labwareId = - commands.find( - (c): c is PickUpTipRunTimeCommand => - c.commandType === 'pickUpTip' && - c.params.pipetteId === result.pipetteId - )?.params.labwareId ?? null - const matchingCommand = - commands.find( - (c): c is LoadLabwareRunTimeCommand => - c.commandType === 'loadLabware' && - c.result != null && - c.result.labwareId === labwareId - ) ?? null - - let tiprackLabwareDef = matchingCommand?.result?.definition ?? null - // We're not prepared to handle labware schema 3 yet. See the todo comment - // in the loadLabware handling, above. - if (tiprackLabwareDef?.schemaVersion === 3) tiprackLabwareDef = null - - const specs: any = getPipetteSpecsV2(command.params.pipetteName) - - const pipetteEntities: PipetteEntities = { - ...acc.pipetteEntities, - [result.pipetteId]: { - name: command.params.pipetteName, - id: command.params.pipetteId, - tiprackLabwareDef: - tiprackLabwareDef != null ? [tiprackLabwareDef] : [], - tiprackDefURI: - tiprackLabwareDef != null - ? [getLabwareDefURI(tiprackLabwareDef)] - : [], - spec: specs, - pythonName: 'n/a', - }, - } - return { - ...acc, - pipetteEntities, - } - } else if ( - command.commandType === 'moveToAddressableArea' || - command.commandType === 'moveToAddressableAreaForDropTip' - ) { - const addressableAreaName = command.params.addressableAreaName - const id = `${uuid()}:${addressableAreaName}` - let location: string = GRIPPER_LOCATION - if (addressableAreaName === 'fixedTrash') { - location = 'cutout12' - } else if (addressableAreaName.includes('WasteChute')) { - location = 'cutoutD3' - } else if (addressableAreaName.includes('movableTrash')) { - location = `cutout${addressableAreaName.split('movableTrash')[1]}` - } - let trashBinEntities: TrashBinEntities = acc.trashBinEntities - if ( - !Object.values(acc.trashBinEntities).some( - entity => entity.location === location - ) - ) { - trashBinEntities = { - ...acc.trashBinEntities, - [id]: { - pythonName: 'trash_bin_1', - id, - location, - }, - } - } - - const wasteChuteEntities: WasteChuteEntities = { - ...acc.wasteChuteEntities, - [id]: { - pythonName: 'waste_chute', - id, - location, - }, - } - return { - ...acc, - trashBinEntities, - wasteChuteEntities, - } - } - - return acc - }, - { - labwareEntities: {}, - moduleEntities: {}, - pipetteEntities: {}, - wasteChuteEntities: {}, - trashBinEntities: {}, - stagingAreaEntities: {}, - // the timeline scrubber doesn't visualize gripper right now - gripperEntities: {}, - // this util is used for the timeline scrubber. It grabs liquid info from analysis - // so this will not be wired up right now - liquidEntities: {}, - config: { OT_PD_DISABLE_MODULE_RESTRICTIONS: true }, - } - ) -} diff --git a/step-generation/src/utils/index.ts b/step-generation/src/utils/index.ts index 195c72f446d..4fa618b9ada 100644 --- a/step-generation/src/utils/index.ts +++ b/step-generation/src/utils/index.ts @@ -21,8 +21,7 @@ export { findThermocyclerProfileRepetitions, } export * from './commandCreatorArgsGetters' -export * from './constructInvariantContextFromRunCommands' -export * from './createTimelineFromRunCommands' +export * from './constructInvariantContextFromAnalysis' export * from './createTimelineFromRunCommands' export * from './heaterShakerCollision' export * from './liquidClassUtils' From 783d29d74f5675fa71721edb42dc49a29988584e Mon Sep 17 00:00:00 2001 From: Jethary Date: Fri, 14 Nov 2025 12:40:45 -0800 Subject: [PATCH 2/5] delete extra spaces --- .../src/utils/constructInvariantContextFromAnalysis.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/step-generation/src/utils/constructInvariantContextFromAnalysis.ts b/step-generation/src/utils/constructInvariantContextFromAnalysis.ts index 8b52ad47e2a..344980e8cb6 100644 --- a/step-generation/src/utils/constructInvariantContextFromAnalysis.ts +++ b/step-generation/src/utils/constructInvariantContextFromAnalysis.ts @@ -32,7 +32,6 @@ export function constructInvariantContextFromAnalysis( const moduleEntities: ModuleEntities = modules.reduce( (acc: ModuleEntities, module) => { const { id, model } = module - acc[id] = { id, type: getModuleType(model), @@ -49,7 +48,6 @@ export function constructInvariantContextFromAnalysis( (acc: LabwareEntities, loadedLabware) => { const { id, definitionUri } = loadedLabware const def = labwareDefinitions[definitionUri] - if (def.schemaVersion === 3) { return acc } @@ -115,7 +113,6 @@ export function constructInvariantContextFromAnalysis( return { ...acc, - stagingAreaEntities: { ...acc.stagingAreaEntities, ...newStagingAreaEntities, @@ -133,7 +130,6 @@ export function constructInvariantContextFromAnalysis( return { ...acc, - stagingAreaEntities: { ...acc.stagingAreaEntities, ...newStagingAreaEntities, From 3a0b2190462c226c4cf92e5cc29ff28d580fe64a Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 17 Nov 2025 07:08:14 -0800 Subject: [PATCH 3/5] fix unit test --- .../__tests__/VisualizerContainer.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/pages/Desktop/Protocols/ProtocolVisualization/__tests__/VisualizerContainer.test.tsx b/app/src/pages/Desktop/Protocols/ProtocolVisualization/__tests__/VisualizerContainer.test.tsx index faed039bec2..10ebabb0356 100644 --- a/app/src/pages/Desktop/Protocols/ProtocolVisualization/__tests__/VisualizerContainer.test.tsx +++ b/app/src/pages/Desktop/Protocols/ProtocolVisualization/__tests__/VisualizerContainer.test.tsx @@ -49,6 +49,9 @@ const mockAnalysis = { 'protocol-designer@chore_release-pd-8.6.0-20251016-222252', source: 'Protocol Designer', }, + modules: [], + labware: [], + pipettes: [], result: 'ok', robotType: 'OT-3 Standard', runTimeParameters: [], From 0de7e25f3649f2bbd03904a175751cc47ce84283 Mon Sep 17 00:00:00 2001 From: Jethary Date: Tue, 18 Nov 2025 07:04:23 -0800 Subject: [PATCH 4/5] address feedback --- .../constructInvariantContextFromAnalysis.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/step-generation/src/utils/constructInvariantContextFromAnalysis.ts b/step-generation/src/utils/constructInvariantContextFromAnalysis.ts index 344980e8cb6..88f4d8240d4 100644 --- a/step-generation/src/utils/constructInvariantContextFromAnalysis.ts +++ b/step-generation/src/utils/constructInvariantContextFromAnalysis.ts @@ -29,43 +29,46 @@ export function constructInvariantContextFromAnalysis( const { labware, modules, pipettes, commands } = analysis const labwareDefinitions = getLabwareDefinitionsByURIForProtocol(commands) - const moduleEntities: ModuleEntities = modules.reduce( - (acc: ModuleEntities, module) => { + const moduleEntities = modules.reduce( + (acc, module) => { const { id, model } = module - acc[id] = { - id, - type: getModuleType(model), - model, - pythonName: 'n/a', - } - return acc + return { + ...acc, + [id]: { + id, + type: getModuleType(model), + model, + pythonName: 'n/a', + } + + } }, {} ) - const labwareEntities: LabwareEntities = labware.reduce( - (acc: LabwareEntities, loadedLabware) => { + const labwareEntities = labware.reduce( + (acc, loadedLabware) => { const { id, definitionUri } = loadedLabware const def = labwareDefinitions[definitionUri] if (def.schemaVersion === 3) { return acc } - - acc[id] = { - id, - labwareDefURI: definitionUri, - def, - pythonName: 'n/a', + return { + ...acc, + [id]: { + id, + labwareDefURI: definitionUri, + def, + pythonName: 'n/a', + }, } - - return acc }, {} ) - const pipetteEntities: PipetteEntities = pipettes.reduce( - (acc: PipetteEntities, pipette) => { + const pipetteEntities = pipettes.reduce( + (acc, pipette) => { const { id, pipetteName } = pipette const spec = getPipetteSpecsV2(pipetteName) const tiprackIdsAssosciatedWithPipette = commands.filter( From 8546f617ead06d2b39fde7f9a53dc4918d1db259 Mon Sep 17 00:00:00 2001 From: Jethary Date: Tue, 18 Nov 2025 07:28:06 -0800 Subject: [PATCH 5/5] fix prettier --- .../constructInvariantContextFromAnalysis.ts | 89 +++++++++---------- 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/step-generation/src/utils/constructInvariantContextFromAnalysis.ts b/step-generation/src/utils/constructInvariantContextFromAnalysis.ts index 88f4d8240d4..5439d892f94 100644 --- a/step-generation/src/utils/constructInvariantContextFromAnalysis.ts +++ b/step-generation/src/utils/constructInvariantContextFromAnalysis.ts @@ -29,23 +29,19 @@ export function constructInvariantContextFromAnalysis( const { labware, modules, pipettes, commands } = analysis const labwareDefinitions = getLabwareDefinitionsByURIForProtocol(commands) - const moduleEntities = modules.reduce( - (acc, module) => { - const { id, model } = module + const moduleEntities = modules.reduce((acc, module) => { + const { id, model } = module - return { - ...acc, - [id]: { - id, - type: getModuleType(model), - model, - pythonName: 'n/a', - } - - } - }, - {} - ) + return { + ...acc, + [id]: { + id, + type: getModuleType(model), + model, + pythonName: 'n/a', + }, + } + }, {}) const labwareEntities = labware.reduce( (acc, loadedLabware) => { @@ -67,40 +63,37 @@ export function constructInvariantContextFromAnalysis( {} ) - const pipetteEntities = pipettes.reduce( - (acc, pipette) => { - const { id, pipetteName } = pipette - const spec = getPipetteSpecsV2(pipetteName) - const tiprackIdsAssosciatedWithPipette = commands.filter( - (command): command is PickUpTipRunTimeCommand => - command.commandType === 'pickUpTip' && command.params.pipetteId === id - ) - const matchingLabwareEntities = tiprackIdsAssosciatedWithPipette.map( - pickUpTipCommand => labwareEntities[pickUpTipCommand.params.labwareId] - ) - const tiprackDefURIs = Array.from( - new Set(matchingLabwareEntities.map(entity => entity.labwareDefURI)) - ) - const tiprackLabwareDefs = Array.from( - new Set(matchingLabwareEntities.map(entity => entity.def)) - ) - if (spec == null) { - return acc - } + const pipetteEntities = pipettes.reduce((acc, pipette) => { + const { id, pipetteName } = pipette + const spec = getPipetteSpecsV2(pipetteName) + const tiprackIdsAssosciatedWithPipette = commands.filter( + (command): command is PickUpTipRunTimeCommand => + command.commandType === 'pickUpTip' && command.params.pipetteId === id + ) + const matchingLabwareEntities = tiprackIdsAssosciatedWithPipette.map( + pickUpTipCommand => labwareEntities[pickUpTipCommand.params.labwareId] + ) + const tiprackDefURIs = Array.from( + new Set(matchingLabwareEntities.map(entity => entity.labwareDefURI)) + ) + const tiprackLabwareDefs = Array.from( + new Set(matchingLabwareEntities.map(entity => entity.def)) + ) + if (spec == null) { + return acc + } - acc[id] = { - name: pipetteName, - id, - tiprackLabwareDef: tiprackLabwareDefs, - tiprackDefURI: tiprackDefURIs, - spec, - pythonName: 'n/a', - } + acc[id] = { + name: pipetteName, + id, + tiprackLabwareDef: tiprackLabwareDefs, + tiprackDefURI: tiprackDefURIs, + spec, + pythonName: 'n/a', + } - return acc - }, - {} - ) + return acc + }, {}) const otherEntities = commands.reduce( ( acc: Omit<