Skip to content

Commit

Permalink
feat(app): Enable labware views for LPC Redesign (#17384)
Browse files Browse the repository at this point in the history
Closes EXEC-1102

This commit refactors the LPC data layer to support labware for the redesign. For the Redesign, the geometric identity (the URI) of each labware drives the flow. That is, a user selects a geometric identity, and is then presented with the option to LPC the "default offset" or one of the "applied location offsets" that occurs in the run. Alternatively the user may just view labware offset information here. After selecting a specific offset for which the user wants to perform LPC, a singular "do LPC for only this offset" flow occurs. Because the robot actually does care about a labware instance for loading/unloading labware, we do keep track of one and only one labwareId for each uri, even if there are multiple labware with the same uri in the run.
  • Loading branch information
mjhuff authored Feb 3, 2025
1 parent f4921c6 commit c841986
Show file tree
Hide file tree
Showing 68 changed files with 1,685 additions and 1,334 deletions.
29 changes: 17 additions & 12 deletions app/src/assets/localization/en/labware_position_check.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"adapter_in_mod_in_slot": "{{adapter}} in {{module}} in {{slot}}",
"adapter_in_slot": "{{adapter}} in {{slot}}",
"adapter_in_tc": "{{adapter}} in {{module}}",
"add": "Add",
"all_modules_and_labware_from_protocol": "All modules and labware used in the protocol {{protocol_name}}",
"applied_location_offsets": "Applied Location Offsets",
"applied_offset_data": "Applied Labware Offset data",
"apply_offset_data": "Apply labware offset data",
"apply_offsets": "apply offsets",
Expand All @@ -24,9 +26,10 @@
"confirm_position_and_move": "Confirm position, move to slot {{next_slot}}",
"confirm_position_and_pick_up_tip": "Confirm position, pick up tip",
"confirm_position_and_return_tip": "Confirm position, return tip to Slot {{next_slot}} and home",
"default_labware_offset": "Default Labware Offset",
"detach_probe": "Remove calibration probe",
"ensure_nozzle_position_odd": "<block>Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap <bold>Move pipette</bold> and then jog the pipette until it is properly aligned.</block>",
"ensure_nozzle_position_desktop": "<block>Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.</block>",
"ensure_nozzle_position_odd": "<block>Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap <bold>Move pipette</bold> and then jog the pipette until it is properly aligned.</block>",
"exit_screen_confirm_exit": "Exit and discard all labware offsets",
"exit_screen_go_back": "Go back to labware position check",
"exit_screen_subtitle": "If you exit now, all labware offsets will be discarded. This cannot be undone.",
Expand All @@ -35,9 +38,10 @@
"install_probe": "Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the <bold>{{location}}</bold> pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
"jog_controls_adjustment": "Need to make an adjustment?",
"jupyter_notebook": "Jupyter Notebook",
"labware": "labware",
"labware_display_location_text": "Deck Slot {{slot}}",
"labware_offset_data": "labware offset data",
"labware_offset": "Labware Offset",
"labware_offset_data": "labware offset data",
"labware_offsets_deleted_warning": "Once you begin Labware Position Check, previously created Labware Offsets will be discarded.",
"labware_offsets_summary_labware": "Labware",
"labware_offsets_summary_location": "Location",
Expand All @@ -46,23 +50,23 @@
"labware_position_check_description": "<block>Labware Position Check is a guided workflow that checks every labware on the deck for an added degree of precision in your protocol.</block><block>Labware Position Check first checks tip racks, and then checks all other labware used in your protocol.</block>",
"labware_position_check_overview": "Labware Position Check Overview",
"labware_position_check_title": "Labware Position Check",
"labware_step_detail_labware_plural": "The tips should be centered above column 1 in <italic>{{labware_name}}</italic> and <bold>level</bold> with the top of the labware.",
"labware_step_detail_labware": "The tip should be centered above A1 in <italic>{{labware_name}}</italic> and <bold>level</bold> with the top of the labware.",
"labware_step_detail_labware_plural": "The tips should be centered above column 1 in <italic>{{labware_name}}</italic> and <bold>level</bold> with the top of the labware.",
"labware_step_detail_link": "See how to tell if the pipette is centered",
"labware_step_detail_modal_heading": "How to tell if the pipette is centered and level",
"labware_step_detail_modal_nozzle": "To ensure that the nozzle is centered, check from a second side of your OT-2.",
"labware_step_detail_modal_nozzle_image_1_text": "Viewed from front, it appears centered...",
"labware_step_detail_modal_nozzle_image_2_nozzle_text": "Nozzle is not centered",
"labware_step_detail_modal_nozzle_image_2_text": "...but viewed from side, it requires adjustment",
"labware_step_detail_modal_nozzle_or_tip": "To ensure the nozzle or tip is level with the top of the labware, position yourself at eye-level and/or slide a sheet of paper between the nozzle and tip.",
"labware_step_detail_modal_nozzle_or_tip_image_1_text": "Viewed from standing height, it appears level...",
"labware_step_detail_modal_nozzle_or_tip_image_2_nozzle_text": "Nozzle is not level",
"labware_step_detail_modal_nozzle_or_tip_image_2_text": "... but viewed from eye-level, it requires adjustment",
"labware_step_detail_modal_nozzle_or_tip_image_3_text": "If you’re having trouble, slide 1 sheet of printer paper between the nozzle and the tip. A single piece of paper should barely pass between them.",
"labware_step_detail_modal_nozzle_or_tip": "To ensure the nozzle or tip is level with the top of the labware, position yourself at eye-level and/or slide a sheet of paper between the nozzle and tip.",
"labware_step_detail_modal_nozzle": "To ensure that the nozzle is centered, check from a second side of your OT-2.",
"labware_step_detail_tiprack_plural": "The pipette nozzles should be centered above column 1 in <italic>{{tiprack_name}}</italic> and <bold>level</bold> with the top of the tips.",
"labware_step_detail_tiprack": "The pipette nozzle should be centered above A1 in <italic>{{tiprack_name}}</italic> and <bold>level</bold> with the top of the tip.",
"labware": "labware",
"labware_step_detail_tiprack_plural": "The pipette nozzles should be centered above column 1 in <italic>{{tiprack_name}}</italic> and <bold>level</bold> with the top of the tips.",
"learn_more": "Learn more",
"legacy_no_offset_data": "No offset data available",
"location": "location",
"lpc_complete_summary_screen_heading": "Labware Position Check Complete",
"module_display_location_text": "{{moduleName}} in Deck Slot {{slot}}",
Expand All @@ -73,10 +77,10 @@
"new_labware_offset_data": "New labware offset data",
"ninety_six_probe_location": "A1 (back left corner)",
"no_labware_offsets": "No Labware Offset",
"no_offset_data": "No offset data",
"no_offset_data_available": "No labware offset data available",
"no_offset_data_on_robot": "This robot has no useable labware offset data for this run.",
"no_offset_data": "No offset data available",
"offsets": "offsets",
"offsets": "Offsets",
"pick_up_tip_from_rack_in_location": "Pick up tip from tip rack in {{location}}",
"picking_up_tip_title": "Picking up tip in slot {{slot}}",
"pipette_nozzle": "pipette nozzle furthest from you",
Expand All @@ -98,13 +102,14 @@
"robot_has_no_offsets_from_previous_runs": "<block>Labware offset data references previous protocol run labware locations to save you time. If all the labware in this protocol have been checked in previous runs, that data will be applied to this run.</block> <block>You can add new offsets with Labware Position Check in later steps.</block>",
"robot_has_offsets_from_previous_runs": "This robot has offsets for labware used in this protocol. If you apply these offsets, you can still adjust them with Labware Position Check.",
"robot_in_motion": "Stand back, robot is in motion.",
"run_labware_position_check": "run labware position check",
"run": "Run",
"run_labware_position_check": "run labware position check",
"secondary_pipette_tipracks_section": "Check tip racks with {{secondary_mount}} Pipette",
"see_how_offsets_work": "See how labware offsets work",
"slot_location": "slot location",
"slot_name": "slot {{slotName}}",
"select_labware_from_list": "Select a labware from the list to check its stored offset data",
"slot": "Slot {{slotName}}",
"slot_location": "Slot Location",
"slot_name": "slot {{slotName}}",
"start_position_check": "begin labware position check, move to Slot {{initial_labware_slot}}",
"stored_offset_data": "Apply Stored Labware Offset Data?",
"stored_offsets_for_this_protocol": "Stored Labware Offset data that applies to this protocol",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { useRobot, useIsFlex } from '/app/redux-resources/robots'
import { useRequiredSetupStepsInOrder } from '/app/redux-resources/runs'
import { useStoredProtocolAnalysis } from '/app/resources/analysis'
import { getMissingSetupSteps } from '/app/redux/protocol-runs'
import { useLPCFlows } from '/app/organisms/LabwarePositionCheck'

import { SetupLabware } from '../SetupLabware'
import { SetupRobotCalibration } from '../SetupRobotCalibration'
Expand Down Expand Up @@ -67,6 +68,7 @@ vi.mock('/app/resources/deck_configuration/hooks')
vi.mock('/app/redux-resources/robots')
vi.mock('/app/redux-resources/runs')
vi.mock('/app/resources/analysis')
vi.mock('/app/organisms/LabwarePositionCheck')
vi.mock('@opentrons/shared-data', async importOriginal => {
const actualSharedData = await importOriginal<typeof SharedData>()
return {
Expand Down Expand Up @@ -186,6 +188,12 @@ describe('ProtocolRunSetup', () => {
when(vi.mocked(useModuleCalibrationStatus))
.calledWith(ROBOT_NAME, RUN_ID)
.thenReturn({ complete: true })
vi.mocked(useLPCFlows).mockReturnValue({
launchLPC: vi.fn(),
lpcProps: null,
showLPC: false,
isLaunchingLPC: false,
})
})
afterEach(() => {
vi.resetAllMocks()
Expand Down
12 changes: 10 additions & 2 deletions app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,28 @@ import type {
RobotType,
CompletedProtocolAnalysis,
DeckConfiguration,
LabwareDefinition2,
} from '@opentrons/shared-data'
import type { LabwareOffset } from '@opentrons/api-client'
import type { LPCLabwareInfo } from '/app/redux/protocol-runs'

// Inject the props specific to the legacy LPC flows, too.
export interface LegacySupportLPCFlowsProps extends LPCFlowsProps {
existingOffsets: LabwareOffset[]
}

export interface LPCFlowsProps {
onCloseClick: () => void
runId: string
robotType: RobotType
deckConfig: DeckConfiguration
existingOffsets: LabwareOffset[]
labwareDefs: LabwareDefinition2[]
labwareInfo: LPCLabwareInfo
mostRecentAnalysis: CompletedProtocolAnalysis
protocolName: string
maintenanceRunId: string
}

export function LPCFlows(props: LPCFlowsProps): JSX.Element {
export function LPCFlows(props: LegacySupportLPCFlowsProps): JSX.Element {
return <LPCWizardContainer {...props} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useLPCLabwareInfo } from './useLPCLabwareInfo'
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import isEqual from 'lodash/isEqual'

import { getLabwareDisplayName, getLabwareDefURI } from '@opentrons/shared-data'

import type { LabwareDefinition2 } from '@opentrons/shared-data'
import type { LPCLabwareInfo, OffsetDetails } from '/app/redux/protocol-runs'
import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos'
import type { UseLPCLabwareInfoProps } from '.'

interface GetLPCLabwareInfoParams {
lwURIs: string[]
currentOffsets: UseLPCLabwareInfoProps['currentOffsets']
lwLocationCombos: LabwareLocationCombo[]
labwareDefs: UseLPCLabwareInfoProps['labwareDefs']
}

export function getLPCLabwareInfoFrom(
params: GetLPCLabwareInfoParams
): LPCLabwareInfo {
return { selectedLabware: null, labware: getLabwareInfoRecords(params) }
}

function getLabwareInfoRecords(
params: GetLPCLabwareInfoParams
): LPCLabwareInfo['labware'] {
const labwareDetails: LPCLabwareInfo['labware'] = {}

params.lwURIs.forEach(uri => {
if (!(uri in labwareDetails)) {
labwareDetails[uri] = {
id: getALabwareIdFromUri({ ...params, uri }),
displayName: getDisplayNameFromUri({ ...params, uri }),
offsetDetails: getOffsetDetailsForLabware({ ...params, uri }),
}
}
})

return labwareDetails
}

type GetLPCLabwareInfoForURI = Omit<GetLPCLabwareInfoParams, 'lwURIs'> & {
uri: string
}

function getALabwareIdFromUri({
uri,
lwLocationCombos,
}: GetLPCLabwareInfoForURI): string {
return (
lwLocationCombos.find(combo => combo.definitionUri === uri)?.labwareId ?? ''
)
}

function getDisplayNameFromUri({
uri,
labwareDefs,
}: GetLPCLabwareInfoForURI): string {
const matchedDef = labwareDefs?.find(
def => getLabwareDefURI(def) === uri
) as LabwareDefinition2

return getLabwareDisplayName(matchedDef)
}

// NOTE: A lot of the logic here acts as temporary adapter that resolves the app's current way of getting offset data (scraping the run record)
// and the end goal of treating labware as first class citizens.
function getOffsetDetailsForLabware({
currentOffsets,
lwLocationCombos,
uri,
}: GetLPCLabwareInfoForURI): OffsetDetails[] {
return lwLocationCombos.flatMap(comboInfo => {
const { definitionUri, location, ...restInfo } = comboInfo

const existingOffset =
currentOffsets.find(
offset =>
uri === offset.definitionUri &&
isEqual(offset.location, comboInfo.location)
) ?? null

return {
existingOffset: existingOffset ?? null,
workingOffset: null,
locationDetails: {
...location,
...restInfo,
definitionUri,
kind: 'location-specific',
},
}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { isEqual } from 'lodash'

import { getLabwareDefURI } from '@opentrons/shared-data'

import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos'

import type {
CompletedProtocolAnalysis,
LabwareDefinition2,
} from '@opentrons/shared-data'
import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos'

export interface GetUniqueLocationComboInfoParams {
protocolData: CompletedProtocolAnalysis | null
labwareDefs: LabwareDefinition2[] | null
}

export function getUniqueLabwareLocationComboInfo({
labwareDefs,
protocolData,
}: GetUniqueLocationComboInfoParams): LabwareLocationCombo[] {
if (protocolData == null || labwareDefs == null) {
return []
}

const { commands, labware, modules = [] } = protocolData
const labwareLocationCombos = getLabwareLocationCombos(
commands,
labware,
modules
)

// Filter out duplicate labware and labware that is not LPC-able.
return labwareLocationCombos.reduce<LabwareLocationCombo[]>(
(acc, labwareLocationCombo) => {
const labwareDef = labwareDefs.find(
def => getLabwareDefURI(def) === labwareLocationCombo.definitionUri
)
if (
(labwareDef?.allowedRoles ?? []).includes('adapter') ||
(labwareDef?.allowedRoles ?? []).includes('lid')
) {
return acc
}
// remove duplicate definitionUri in same location
const comboAlreadyExists = acc.some(
accLocationCombo =>
labwareLocationCombo.definitionUri ===
accLocationCombo.definitionUri &&
isEqual(labwareLocationCombo.location, accLocationCombo.location)
)
return comboAlreadyExists ? acc : [...acc, labwareLocationCombo]
},
[]
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useMemo } from 'react'

import { getUniqueLabwareLocationComboInfo } from './getUniqueLabwareLocationComboInfo'
import { getLPCLabwareInfoFrom } from './getLPCLabwareInfoFrom'

import type { LabwareOffset } from '@opentrons/api-client'
import type { LPCLabwareInfo } from '/app/redux/protocol-runs'
import type { GetUniqueLocationComboInfoParams } from './getUniqueLabwareLocationComboInfo'

export type UseLPCLabwareInfoProps = GetUniqueLocationComboInfoParams & {
currentOffsets: LabwareOffset[]
}

// TODO(jh, 01-22-25): This interface will change substantially the switch to /labwareOffsets.

// Structures LPC-able labware info for injection into LPC flows.
export function useLPCLabwareInfo({
currentOffsets,
labwareDefs,
protocolData,
}: UseLPCLabwareInfoProps): LPCLabwareInfo {
// Analysis-derived data is the source of truth, because we must account for labware that has offsets AND account for labware
// that does not have offsets. This will change with the LPC HTTP API refactors.
const lwURIs = getLabwareURIsFromAnalysis(protocolData)
const lwLocationCombos = useMemo(
() =>
getUniqueLabwareLocationComboInfo({
labwareDefs,
protocolData,
}),
[labwareDefs != null, protocolData != null]
)

return useMemo(
() =>
getLPCLabwareInfoFrom({
lwURIs,
currentOffsets,
lwLocationCombos,
labwareDefs,
}),
[lwURIs.length, currentOffsets.length, lwLocationCombos.length]
)
}

function getLabwareURIsFromAnalysis(
analysis: UseLPCLabwareInfoProps['protocolData']
): string[] {
return analysis?.labware.map(lwInfo => lwInfo.definitionUri) ?? []
}
Loading

0 comments on commit c841986

Please sign in to comment.