Skip to content

Commit d3bd462

Browse files
authored
fix(app): fix labware well color and liquid volume (#20814)
Closes [RABR-856](https://opentrons.atlassian.net/browse/RABR-856)
1 parent a6d0cdf commit d3bd462

File tree

1 file changed

+93
-71
lines changed

1 file changed

+93
-71
lines changed

shared-data/js/helpers/getLabwareInfoByLiquidId.ts

Lines changed: 93 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,25 @@ import reduce from 'lodash/reduce'
33
import type { LoadLiquidRunTimeCommand, RunTimeCommand } from '../../command'
44

55
export interface LabwareByLiquidId {
6-
[liquidId: string]: Array<{
7-
labwareId: string
8-
volumeByWell: { [well: string]: number }
9-
}>
6+
[liquidId: string]: LabwareVolumeEntry[]
107
}
8+
9+
interface WellVolumeMap {
10+
[well: string]: number
11+
}
12+
13+
interface LabwareVolumeEntry {
14+
labwareId: string
15+
volumeByWell: WellVolumeMap
16+
}
17+
1118
interface WellVolumeEntry {
1219
labwareId: string
1320
volume: number
1421
liquidId: string
1522
}
23+
24+
// Filter for loadLiquid commands with non-empty liquids, then build and consolidate them
1625
export function getLabwareInfoByLiquidId(
1726
commands: RunTimeCommand[]
1827
): LabwareByLiquidId {
@@ -25,19 +34,29 @@ export function getLabwareInfoByLiquidId(
2534
)
2635
: []
2736

28-
const initialLabwareByLiquidId = reduce<
29-
LoadLiquidRunTimeCommand,
30-
LabwareByLiquidId
31-
>(
32-
loadLiquidCommands,
37+
const initialLabwareByLiquidId =
38+
buildInitialLabwareByLiquidId(loadLiquidCommands)
39+
40+
return consolidateSharedWells(initialLabwareByLiquidId)
41+
}
42+
43+
// Group labware by liquidId, merging volumeByWell entries when the same labware
44+
// appears multiple times for the same liquid
45+
function buildInitialLabwareByLiquidId(
46+
commands: LoadLiquidRunTimeCommand[]
47+
): LabwareByLiquidId {
48+
return reduce<LoadLiquidRunTimeCommand, LabwareByLiquidId>(
49+
commands,
3350
(acc, command) => {
3451
const { liquidId, labwareId, volumeByWell } = command.params
3552
if (!(liquidId in acc)) acc[liquidId] = []
3653

3754
const labwareIndex = acc[liquidId].findIndex(
3855
i => i.labwareId === labwareId
3956
)
57+
4058
if (labwareIndex >= 0) {
59+
// Merge wells into existing labware entry
4160
acc[liquidId][labwareIndex].volumeByWell = {
4261
...acc[liquidId][labwareIndex].volumeByWell,
4362
...volumeByWell,
@@ -49,72 +68,75 @@ export function getLabwareInfoByLiquidId(
4968
},
5069
{}
5170
)
71+
}
72+
73+
// Build a reverse mapping of wells to all liquids that occupy them
74+
function consolidateSharedWells(
75+
liquidsByIdForLabware: LabwareByLiquidId
76+
): LabwareByLiquidId {
77+
const wellToLabwareVolumes: Record<string, WellVolumeEntry[]> = {}
5278

53-
const consolidateSharedWells = (
54-
liquidsByIdForLabware: LabwareByLiquidId
55-
): LabwareByLiquidId => {
56-
const wellToLabwareVolumes: Record<string, WellVolumeEntry[]> = {}
57-
58-
// Track all wells with their labware, volume, and source liquid
59-
Object.entries(liquidsByIdForLabware).forEach(
60-
([liquidId, labwareArray]) => {
61-
labwareArray.forEach(labware => {
62-
Object.entries(labware.volumeByWell).forEach(([well, volume]) => {
63-
wellToLabwareVolumes[well] ??= []
64-
wellToLabwareVolumes[well].push({
65-
labwareId: labware.labwareId,
66-
volume,
67-
liquidId,
68-
})
69-
})
79+
Object.entries(liquidsByIdForLabware).forEach(([liquidId, labwareArray]) => {
80+
labwareArray.forEach(labware => {
81+
Object.entries(labware.volumeByWell).forEach(([well, volume]) => {
82+
const compositeKey = `${labware.labwareId}:${well}`
83+
wellToLabwareVolumes[compositeKey] ??= []
84+
wellToLabwareVolumes[compositeKey].push({
85+
labwareId: labware.labwareId,
86+
volume,
87+
liquidId,
7088
})
71-
}
72-
)
73-
74-
// Identify overlapping wells
75-
const overlappingWells = Object.entries(wellToLabwareVolumes).filter(
76-
([, entries]) => entries.length > 1
77-
)
78-
const consolidatedWells: LabwareByLiquidId = {}
79-
80-
overlappingWells.forEach(([well, entries]) => {
81-
const totalVolume = entries.reduce((sum, entry) => sum + entry.volume, 0)
82-
const labwareIdName = entries[0].labwareId
83-
const liquidIds = entries.map(e => e.liquidId).sort()
84-
const mixedLiquidId = `mixed-${liquidIds.join('-')}`
85-
86-
consolidatedWells[mixedLiquidId] ??= [
87-
{ labwareId: labwareIdName, volumeByWell: {} },
88-
]
89-
consolidatedWells[mixedLiquidId][0].volumeByWell[well] = totalVolume
89+
})
9090
})
91-
// Remove consolidated wells from the original liquids
92-
const cleanedOriginal: LabwareByLiquidId = {}
93-
const overlappingWellSet = new Set(overlappingWells.map(([well]) => well))
94-
95-
Object.entries(liquidsByIdForLabware).forEach(
96-
([liquidId, labwareArray]) => {
97-
labwareArray.forEach(labware => {
98-
const remainingWells = Object.fromEntries(
99-
Object.entries(labware.volumeByWell).filter(
100-
([well]) => !overlappingWellSet.has(well)
101-
)
102-
)
103-
104-
if (Object.keys(remainingWells).length > 0) {
105-
cleanedOriginal[liquidId] ??= []
106-
cleanedOriginal[liquidId].push({
107-
labwareId: labware.labwareId,
108-
volumeByWell: remainingWells,
109-
})
110-
}
91+
})
92+
93+
// Find wells that have been loaded with multiple different liquids
94+
const overlappingWells = Object.entries(wellToLabwareVolumes).filter(
95+
([, entries]) => entries.length > 1
96+
)
97+
98+
// Create consolidated entries for wells with mixed liquids
99+
const consolidatedWells: LabwareByLiquidId = {}
100+
101+
overlappingWells.forEach(([compositeKey, entries]) => {
102+
const totalVolume = entries.reduce((sum, entry) => sum + entry.volume, 0)
103+
const labwareIdName = entries[0].labwareId
104+
const liquidIds = entries.map(e => e.liquidId).sort()
105+
const mixedLiquidId = `mixed-${liquidIds.join('-')}`
106+
107+
consolidatedWells[mixedLiquidId] ??= [
108+
{ labwareId: labwareIdName, volumeByWell: {} },
109+
]
110+
const wellName = compositeKey.split(':')[1]
111+
consolidatedWells[mixedLiquidId][0].volumeByWell[wellName] = totalVolume
112+
})
113+
114+
// Remove overlapping wells from original liquid entries, keeping only non-overlapping wells
115+
const cleanedOriginal: LabwareByLiquidId = {}
116+
const overlappingWellSet = new Set(
117+
overlappingWells.map(([compositeKey]) => compositeKey)
118+
)
119+
120+
Object.entries(liquidsByIdForLabware).forEach(([liquidId, labwareArray]) => {
121+
labwareArray.forEach(labware => {
122+
const remainingWells = Object.fromEntries(
123+
Object.entries(labware.volumeByWell).filter(
124+
([well]) => !overlappingWellSet.has(`${labware.labwareId}:${well}`)
125+
)
126+
)
127+
128+
if (Object.keys(remainingWells).length > 0) {
129+
cleanedOriginal[liquidId] ??= []
130+
cleanedOriginal[liquidId].push({
131+
labwareId: labware.labwareId,
132+
volumeByWell: remainingWells,
111133
})
112134
}
113-
)
114-
return {
115-
...cleanedOriginal,
116-
...consolidatedWells,
117-
}
135+
})
136+
})
137+
138+
return {
139+
...cleanedOriginal,
140+
...consolidatedWells,
118141
}
119-
return consolidateSharedWells(initialLabwareByLiquidId)
120142
}

0 commit comments

Comments
 (0)