Skip to content

Commit 1b1dd94

Browse files
authored
fix(app): do not select a default location that contains a fixture (#18508)
Closes RQA-4243 It's insufficient to only check module locations for the presence of an object on the deck, because doing so doesn't consider fixtures. Let's make things simpler: if a module is known to be in C2, using the LPC-able labware, let's find the labware that doesn't have a module beneath it and use that one.
1 parent 444c3ab commit 1b1dd94

File tree

2 files changed

+90
-76
lines changed

2 files changed

+90
-76
lines changed

app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/getLPCLabwareInfoFrom/__tests__/getDefaultOffsetForLabware.test.ts

Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { vi, it, describe, expect } from 'vitest'
22

33
import { ANY_LOCATION } from '@opentrons/api-client'
4-
import { C2_ADDRESSABLE_AREA, getLabwareDefURI } from '@opentrons/shared-data'
4+
import {
5+
C3_ADDRESSABLE_AREA,
6+
C2_ADDRESSABLE_AREA,
7+
getLabwareDefURI,
8+
} from '@opentrons/shared-data'
59

610
import { getDefaultOffsetDetailsForLabware } from '../getDefaultOffsetForLabware'
711
import { OFFSET_KIND_DEFAULT } from '/app/redux/protocol-runs'
@@ -25,7 +29,27 @@ describe('getDefaultOffsetDetailsForLabware', () => {
2529
const ADAPTER_URI = 'opentrons/adapter-1'
2630

2731
const MOCK_LW_LOC_COMBOS = [
28-
{ definitionUri: LABWARE_URI, labwareId: LABWARE_ID },
32+
{
33+
definitionUri: LABWARE_URI,
34+
labwareId: LABWARE_ID,
35+
addressableAreaName: 'A1',
36+
closestBeneathModuleId: null,
37+
},
38+
]
39+
40+
const MOCK_LW_LOC_COMBOS_WITH_MODULE = [
41+
{
42+
definitionUri: LABWARE_URI,
43+
labwareId: LABWARE_ID,
44+
addressableAreaName: 'A1',
45+
closestBeneathModuleId: 'module-123',
46+
},
47+
{
48+
definitionUri: 'other-labware',
49+
labwareId: 'other-labware-id',
50+
addressableAreaName: 'B2',
51+
closestBeneathModuleId: null,
52+
},
2953
]
3054

3155
const MOCK_OFFSET: StoredLabwareOffset = {
@@ -74,7 +98,7 @@ describe('getDefaultOffsetDetailsForLabware', () => {
7498
modules: [],
7599
}
76100

77-
const MOCK_PROTOCOL_DATA_WITH_MODULES = {
101+
const MOCK_PROTOCOL_DATA_WITH_C2_MODULE = {
78102
labware: [
79103
{
80104
id: ADAPTER_ID,
@@ -85,9 +109,6 @@ describe('getDefaultOffsetDetailsForLabware', () => {
85109
{
86110
location: { slotName: 'C2' },
87111
},
88-
{
89-
location: { slotName: 'A1' },
90-
},
91112
],
92113
}
93114

@@ -235,59 +256,61 @@ describe('getDefaultOffsetDetailsForLabware', () => {
235256
expect(result.locationDetails.addressableAreaName).toBe(C2_ADDRESSABLE_AREA)
236257
})
237258

238-
it('should use alternative slot when C2 is occupied by a module', () => {
259+
it('should find first location with no module when C2 is occupied', () => {
239260
const result = getDefaultOffsetDetailsForLabware({
240261
uri: LABWARE_URI,
241-
lwLocInfo: MOCK_LW_LOC_COMBOS,
262+
lwLocInfo: MOCK_LW_LOC_COMBOS_WITH_MODULE,
242263
currentOffsets: [],
243264
labwareDefs: [MOCK_LABWARE_DEF],
244265
locationSpecificOffsetDetails: [],
245-
protocolData: MOCK_PROTOCOL_DATA_WITH_MODULES,
266+
protocolData: MOCK_PROTOCOL_DATA_WITH_C2_MODULE,
246267
} as any)
247268

248-
expect(result.locationDetails.addressableAreaName).not.toBe(
249-
C2_ADDRESSABLE_AREA
250-
)
251-
expect([
252-
'A2',
253-
'A3',
254-
'B1',
255-
'B2',
256-
'B3',
257-
'C1',
258-
'C3',
259-
'D1',
260-
'D2',
261-
'D3',
262-
]).toContain(result.locationDetails.addressableAreaName)
269+
expect(result.locationDetails.addressableAreaName).toBe('B2')
263270
})
264271

265-
it('should fallback to C2 when all slots are occupied', () => {
266-
const protocolDataWithAllModules = {
267-
labware: [],
268-
modules: [
269-
{ location: { slotName: 'A1' } },
270-
{ location: { slotName: 'A2' } },
271-
{ location: { slotName: 'A3' } },
272-
{ location: { slotName: 'B1' } },
273-
{ location: { slotName: 'B2' } },
274-
{ location: { slotName: 'B3' } },
275-
{ location: { slotName: 'C1' } },
276-
{ location: { slotName: 'C2' } },
277-
{ location: { slotName: 'C3' } },
278-
{ location: { slotName: 'D1' } },
279-
{ location: { slotName: 'D2' } },
280-
{ location: { slotName: 'D3' } },
281-
],
282-
}
272+
it('should fallback to C3 when C2 is occupied and no locations without modules are found', () => {
273+
const lwLocInfoAllWithModules = [
274+
{
275+
definitionUri: LABWARE_URI,
276+
labwareId: LABWARE_ID,
277+
addressableAreaName: 'A1' as any,
278+
closestBeneathModuleId: 'module-1',
279+
},
280+
{
281+
definitionUri: 'other-labware',
282+
labwareId: 'other-labware-id',
283+
addressableAreaName: 'B2' as any,
284+
closestBeneathModuleId: 'module-2',
285+
},
286+
]
287+
288+
const result = getDefaultOffsetDetailsForLabware({
289+
uri: LABWARE_URI,
290+
lwLocInfo: lwLocInfoAllWithModules,
291+
currentOffsets: [],
292+
labwareDefs: [MOCK_LABWARE_DEF],
293+
locationSpecificOffsetDetails: [],
294+
protocolData: MOCK_PROTOCOL_DATA_WITH_C2_MODULE,
295+
} as any)
296+
297+
expect(result.locationDetails.addressableAreaName).toBe(C3_ADDRESSABLE_AREA)
298+
})
283299

300+
it('should return C2 when C2 is not occupied by modules', () => {
284301
const result = getDefaultOffsetDetailsForLabware({
285302
uri: LABWARE_URI,
286303
lwLocInfo: MOCK_LW_LOC_COMBOS,
287304
currentOffsets: [],
288305
labwareDefs: [MOCK_LABWARE_DEF],
289306
locationSpecificOffsetDetails: [],
290-
protocolData: protocolDataWithAllModules,
307+
protocolData: {
308+
labware: [],
309+
modules: [
310+
{ location: { slotName: 'A1' } },
311+
{ location: { slotName: 'B1' } },
312+
],
313+
},
291314
} as any)
292315

293316
expect(result.locationDetails.addressableAreaName).toBe(C2_ADDRESSABLE_AREA)

app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/getLPCLabwareInfoFrom/getDefaultOffsetForLabware.ts

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
1-
import head from 'lodash/head'
2-
31
import { ANY_LOCATION } from '@opentrons/api-client'
42
import {
5-
A1_ADDRESSABLE_AREA,
6-
B1_ADDRESSABLE_AREA,
73
C2_ADDRESSABLE_AREA,
4+
C3_ADDRESSABLE_AREA,
85
getLabwareDefURI,
9-
STANDARD_FLEX_SLOTS,
10-
THERMOCYCLER_MODULE_V1,
11-
THERMOCYCLER_MODULE_V2,
126
} from '@opentrons/shared-data'
137

148
import { OFFSET_KIND_DEFAULT } from '/app/redux/protocol-runs'
159

16-
import type {
17-
FlexAddressableAreaName,
18-
LabwareDefinition2,
19-
} from '@opentrons/shared-data'
2010
import type {
2111
DefaultOffsetDetails,
12+
LabwareLocationInfo,
2213
LabwareModuleStackupDetails,
2314
LocationSpecificOffsetDetails,
2415
} from '/app/redux/protocol-runs'
2516
import type { GetLPCLabwareInfoForURI } from '.'
17+
import type {
18+
FlexAddressableAreaName,
19+
LabwareDefinition2,
20+
} from '@opentrons/shared-data'
2621

2722
interface GetDefaultOffsetDetailsForLabwareParams
2823
extends GetLPCLabwareInfoForURI {
@@ -55,7 +50,10 @@ export function getDefaultOffsetDetailsForLabware(
5550
definitionUri: uri,
5651
kind: OFFSET_KIND_DEFAULT,
5752
// We always do default offset LPCing in this slot.
58-
addressableAreaName: getValidDefaultOffsetLocation(params.protocolData),
53+
addressableAreaName: getValidDefaultOffsetLocation(
54+
params.lwLocInfo,
55+
params.protocolData
56+
),
5957
lwOffsetLocSeq: ANY_LOCATION,
6058
closestBeneathAdapterId,
6159
// The only labware present on deck when configuring the default offset is the top-most labware itself.
@@ -151,29 +149,22 @@ function getFirstAdapterIdFrom(
151149
// NOTE: This util is meant to be temporary until product/design devise
152150
// an alternative method for default offset location selection.
153151
function getValidDefaultOffsetLocation(
152+
lwLocInfo: LabwareLocationInfo[],
154153
protocolData: GetStackingInfoParams['protocolData']
155154
): FlexAddressableAreaName {
156-
const validLocations = new Set<FlexAddressableAreaName>(
157-
STANDARD_FLEX_SLOTS as FlexAddressableAreaName[]
158-
)
159-
160-
protocolData?.modules.forEach(mod => {
161-
if (
162-
mod.model === THERMOCYCLER_MODULE_V2 ||
163-
mod.model === THERMOCYCLER_MODULE_V1
164-
) {
165-
validLocations.delete(A1_ADDRESSABLE_AREA)
166-
validLocations.delete(B1_ADDRESSABLE_AREA)
167-
} else {
168-
const slotName = mod.location.slotName as FlexAddressableAreaName
169-
validLocations.delete(slotName)
170-
}
171-
})
172-
173-
if (validLocations.has(C2_ADDRESSABLE_AREA)) {
174-
return C2_ADDRESSABLE_AREA
155+
const isSlotC2Unavailable =
156+
protocolData?.modules.some(
157+
mod => mod.location.slotName === C2_ADDRESSABLE_AREA
158+
) ?? false
159+
160+
if (isSlotC2Unavailable) {
161+
// Given all the LPC-able labware, find the first slot in which no module is
162+
// loaded beneath that labware.
163+
const locationWithNoModule = lwLocInfo.find(
164+
aLwLocInfo => aLwLocInfo.closestBeneathModuleId == null
165+
)
166+
return locationWithNoModule?.addressableAreaName ?? C3_ADDRESSABLE_AREA
175167
} else {
176-
// The fallback should never occur in practice.
177-
return head(Array.from(validLocations)) ?? C2_ADDRESSABLE_AREA
168+
return C2_ADDRESSABLE_AREA
178169
}
179170
}

0 commit comments

Comments
 (0)