@opentrons/shared-data 0.1.2-alpha.0 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/js/__tests__/__snapshots__/pipettes.test.ts.snap +5969 -0
  2. package/js/__tests__/deckSchemas.test.ts +87 -0
  3. package/js/__tests__/errors.test.js +21 -0
  4. package/js/__tests__/getAreSlotsAdjacent.test.ts +128 -0
  5. package/js/__tests__/getWellNamePerMultiTip.test.ts +162 -0
  6. package/js/__tests__/labwareDefQuirks.test.ts +38 -0
  7. package/js/__tests__/labwareDefSchemaV1.test.ts +82 -0
  8. package/js/__tests__/labwareDefSchemaV2.test.ts +298 -0
  9. package/js/__tests__/moduleAccessors.test.ts +50 -0
  10. package/js/__tests__/moduleSpecsSchema.test.ts +77 -0
  11. package/js/__tests__/pipetteSchemaV2.test.ts +107 -0
  12. package/js/__tests__/pipetteSpecSchemas.test.ts +48 -0
  13. package/js/__tests__/pipettes.test.ts +58 -0
  14. package/js/__tests__/protocolSchemaV4.test.ts +137 -0
  15. package/js/__tests__/protocolSchemaV5.test.ts +137 -0
  16. package/js/__tests__/protocolSchemaV6.test.ts +132 -0
  17. package/js/__tests__/protocolSchemaV7.test.ts +64 -0
  18. package/js/__tests__/protocolValidation.test.ts +83 -0
  19. package/js/__tests__/sortWells.test.ts +40 -0
  20. package/js/__tests__/splitWellsOnColumn.test.ts +21 -0
  21. package/js/__tests__/validateErrors.test.js +34 -0
  22. package/js/constants.ts +405 -0
  23. package/js/cypressUtils.ts +35 -0
  24. package/js/errors.ts +33 -0
  25. package/js/fixtures.ts +156 -0
  26. package/js/getLabware.ts +145 -0
  27. package/js/gripper.ts +29 -0
  28. package/js/helpers/__tests__/getAdapterName.test.ts +31 -0
  29. package/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts +12 -0
  30. package/js/helpers/__tests__/getSimplestFlexDeckConfig.test.ts +176 -0
  31. package/js/helpers/__tests__/getVectorDifference.test.ts +25 -0
  32. package/js/helpers/__tests__/getVectorSum.test.ts +21 -0
  33. package/js/helpers/__tests__/labwareInference.test.ts +84 -0
  34. package/js/helpers/__tests__/orderWells.test.ts +87 -0
  35. package/js/helpers/__tests__/parseProtocolData.test.ts +165 -0
  36. package/js/helpers/__tests__/volume.test.ts +108 -0
  37. package/js/helpers/__tests__/wellSets.test.ts +352 -0
  38. package/js/helpers/get96Channel384WellPlateWells.ts +44 -0
  39. package/js/helpers/getAddressableAreasInProtocol.ts +98 -0
  40. package/js/helpers/getLoadedLabwareDefinitionsByUri.ts +30 -0
  41. package/js/helpers/getModuleVizDims.ts +118 -0
  42. package/js/helpers/getOccludedSlotCountForModule.ts +13 -0
  43. package/js/helpers/getSimplestFlexDeckConfig.ts +183 -0
  44. package/js/helpers/getVectorDifference.ts +12 -0
  45. package/js/helpers/getVectorSum.ts +12 -0
  46. package/js/helpers/getWellNamePerMultiTip.ts +86 -0
  47. package/js/helpers/getWellTotalVolume.ts +16 -0
  48. package/js/helpers/index.ts +348 -0
  49. package/js/helpers/labwareInference.ts +92 -0
  50. package/js/helpers/orderWells.ts +69 -0
  51. package/js/helpers/parseProtocolData.ts +141 -0
  52. package/js/helpers/volume.ts +35 -0
  53. package/js/helpers/wellIsRect.ts +6 -0
  54. package/js/helpers/wellSets.ts +155 -0
  55. package/js/index.ts +14 -0
  56. package/js/labwareTools/__tests__/__snapshots__/createIrregularLabware.test.ts.snap +3 -0
  57. package/js/labwareTools/__tests__/__snapshots__/createLabware.test.ts.snap +3 -0
  58. package/js/labwareTools/__tests__/createDefaultDisplayName.test.ts +111 -0
  59. package/js/labwareTools/__tests__/createIrregularLabware.test.ts +226 -0
  60. package/js/labwareTools/__tests__/createLabware.test.ts +147 -0
  61. package/js/labwareTools/index.ts +452 -0
  62. package/js/modules.ts +97 -0
  63. package/js/pipettes.ts +89 -0
  64. package/js/protocols.ts +290 -0
  65. package/js/schema.ts +80 -0
  66. package/js/scripts/generateDeckLayersFromSVG.js +46 -0
  67. package/js/titleCase.ts +30 -0
  68. package/js/types.ts +561 -0
  69. package/lib/js/__tests__/deckSchemas.test.d.ts +1 -0
  70. package/lib/js/__tests__/getAreSlotsAdjacent.test.d.ts +1 -0
  71. package/lib/js/__tests__/getWellNamePerMultiTip.test.d.ts +1 -0
  72. package/lib/js/__tests__/labwareDefQuirks.test.d.ts +1 -0
  73. package/lib/js/__tests__/labwareDefSchemaV1.test.d.ts +1 -0
  74. package/lib/js/__tests__/labwareDefSchemaV2.test.d.ts +1 -0
  75. package/lib/js/__tests__/moduleAccessors.test.d.ts +1 -0
  76. package/lib/js/__tests__/moduleSpecsSchema.test.d.ts +1 -0
  77. package/lib/js/__tests__/pipetteSchemaV2.test.d.ts +1 -0
  78. package/lib/js/__tests__/pipetteSpecSchemas.test.d.ts +1 -0
  79. package/lib/js/__tests__/pipettes.test.d.ts +1 -0
  80. package/lib/js/__tests__/protocolSchemaV4.test.d.ts +1 -0
  81. package/lib/js/__tests__/protocolSchemaV5.test.d.ts +1 -0
  82. package/lib/js/__tests__/protocolSchemaV6.test.d.ts +1 -0
  83. package/lib/js/__tests__/protocolSchemaV7.test.d.ts +1 -0
  84. package/lib/js/__tests__/protocolValidation.test.d.ts +1 -0
  85. package/lib/js/__tests__/sortWells.test.d.ts +1 -0
  86. package/lib/js/__tests__/splitWellsOnColumn.test.d.ts +1 -0
  87. package/lib/js/constants.d.ts +156 -0
  88. package/lib/js/cypressUtils.d.ts +1 -0
  89. package/lib/js/errors.d.ts +13 -0
  90. package/lib/js/fixtures.d.ts +13 -0
  91. package/lib/js/getLabware.d.ts +8 -0
  92. package/lib/js/gripper.d.ts +3 -0
  93. package/lib/js/helpers/__tests__/getAdapterName.test.d.ts +1 -0
  94. package/lib/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.d.ts +1 -0
  95. package/lib/js/helpers/__tests__/getSimplestFlexDeckConfig.test.d.ts +1 -0
  96. package/lib/js/helpers/__tests__/getVectorDifference.test.d.ts +1 -0
  97. package/lib/js/helpers/__tests__/getVectorSum.test.d.ts +1 -0
  98. package/lib/js/helpers/__tests__/labwareInference.test.d.ts +1 -0
  99. package/lib/js/helpers/__tests__/orderWells.test.d.ts +1 -0
  100. package/lib/js/helpers/__tests__/parseProtocolData.test.d.ts +1 -0
  101. package/lib/js/helpers/__tests__/volume.test.d.ts +1 -0
  102. package/lib/js/helpers/__tests__/wellSets.test.d.ts +1 -0
  103. package/lib/js/helpers/get96Channel384WellPlateWells.d.ts +1 -0
  104. package/lib/js/helpers/getAddressableAreasInProtocol.d.ts +4 -0
  105. package/lib/js/helpers/getLoadedLabwareDefinitionsByUri.d.ts +5 -0
  106. package/lib/js/helpers/getModuleVizDims.d.ts +16 -0
  107. package/lib/js/helpers/getOccludedSlotCountForModule.d.ts +2 -0
  108. package/lib/js/helpers/getSimplestFlexDeckConfig.d.ts +14 -0
  109. package/lib/js/helpers/getVectorDifference.d.ts +2 -0
  110. package/lib/js/helpers/getVectorSum.d.ts +2 -0
  111. package/lib/js/helpers/getWellNamePerMultiTip.d.ts +3 -0
  112. package/lib/js/helpers/getWellTotalVolume.d.ts +2 -0
  113. package/lib/js/helpers/index.d.ts +44 -0
  114. package/lib/js/helpers/labwareInference.d.ts +16 -0
  115. package/lib/js/helpers/orderWells.d.ts +2 -0
  116. package/lib/js/helpers/parseProtocolData.d.ts +23 -0
  117. package/lib/js/helpers/volume.d.ts +4 -0
  118. package/lib/js/helpers/wellIsRect.d.ts +3 -0
  119. package/lib/js/helpers/wellSets.d.ts +9 -0
  120. package/lib/js/index.d.ts +14 -0
  121. package/lib/js/labwareTools/__tests__/createDefaultDisplayName.test.d.ts +1 -0
  122. package/lib/js/labwareTools/__tests__/createIrregularLabware.test.d.ts +1 -0
  123. package/lib/js/labwareTools/__tests__/createLabware.test.d.ts +1 -0
  124. package/lib/js/labwareTools/index.d.ts +65 -0
  125. package/lib/js/modules.d.ts +6 -0
  126. package/lib/js/pipettes.d.ts +13 -0
  127. package/lib/js/protocols.d.ts +3 -0
  128. package/lib/js/schema.d.ts +90 -0
  129. package/lib/js/titleCase.d.ts +1 -0
  130. package/lib/js/types.d.ts +398 -0
  131. package/package.json +1 -1
@@ -0,0 +1,348 @@
1
+ import assert from 'assert'
2
+ import uniq from 'lodash/uniq'
3
+
4
+ import { OPENTRONS_LABWARE_NAMESPACE } from '../constants'
5
+ import standardOt2DeckDef from '../../deck/definitions/4/ot2_standard.json'
6
+ import standardFlexDeckDef from '../../deck/definitions/4/ot3_standard.json'
7
+ import type {
8
+ DeckDefinition,
9
+ LabwareDefinition2,
10
+ ModuleModel,
11
+ RobotType,
12
+ ThermalAdapterName,
13
+ } from '../types'
14
+
15
+ export { getWellNamePerMultiTip } from './getWellNamePerMultiTip'
16
+ export { getWellTotalVolume } from './getWellTotalVolume'
17
+ export { wellIsRect } from './wellIsRect'
18
+ export { orderWells } from './orderWells'
19
+ export { get96Channel384WellPlateWells } from './get96Channel384WellPlateWells'
20
+
21
+ export * from './parseProtocolData'
22
+ export * from './volume'
23
+ export * from './wellSets'
24
+ export * from './getModuleVizDims'
25
+ export * from './getVectorDifference'
26
+ export * from './getVectorSum'
27
+ export * from './getLoadedLabwareDefinitionsByUri'
28
+ export * from './getOccludedSlotCountForModule'
29
+ export * from './labwareInference'
30
+ export * from './getAddressableAreasInProtocol'
31
+ export * from './getSimplestFlexDeckConfig'
32
+
33
+ export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean =>
34
+ def?.namespace === OPENTRONS_LABWARE_NAMESPACE
35
+
36
+ export const getLabwareDefURI = (def: LabwareDefinition2): string =>
37
+ constructLabwareDefURI(
38
+ def.namespace,
39
+ def.parameters.loadName,
40
+ String(def.version)
41
+ )
42
+
43
+ export const constructLabwareDefURI = (
44
+ namespace: string,
45
+ loadName: string,
46
+ version: string
47
+ ): string => `${namespace}/${loadName}/${version}`
48
+
49
+ // Load names of "retired" labware
50
+ // TODO(mc, 2019-12-3): how should this correspond to LABWAREV2_DO_NOT_LIST?
51
+ // see shared-data/js/getLabware.js
52
+ const RETIRED_LABWARE = [
53
+ 'geb_96_tiprack_10ul',
54
+ 'geb_96_tiprack_1000ul',
55
+ 'opentrons_1_trash_850ml_fixed',
56
+ 'opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic',
57
+ 'opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic',
58
+ 'opentrons_24_tuberack_generic_0.75ml_snapcap_acrylic',
59
+ 'opentrons_40_aluminumblock_eppendorf_24x2ml_safelock_snapcap_generic_16x0.2ml_pcr_strip',
60
+ 'opentrons_96_aluminumblock_biorad_wellplate_200ul',
61
+ 'tipone_96_tiprack_200ul',
62
+ 'eppendorf_96_tiprack_1000ul_eptips',
63
+ 'eppendorf_96_tiprack_10ul_eptips',
64
+ // Replaced by opentrons_96_wellplate_200ul_pcr_full_skirt
65
+ // https://opentrons.atlassian.net/browse/RLAB-230
66
+ 'armadillo_96_wellplate_200ul_pcr_full_skirt',
67
+ ]
68
+
69
+ export const getLabwareDisplayName = (
70
+ labwareDef: LabwareDefinition2
71
+ ): string => {
72
+ const { displayName } = labwareDef.metadata
73
+
74
+ if (
75
+ getLabwareDefIsStandard(labwareDef) &&
76
+ RETIRED_LABWARE.includes(labwareDef.parameters.loadName)
77
+ ) {
78
+ return `(Retired) ${displayName}`
79
+ }
80
+
81
+ return displayName
82
+ }
83
+
84
+ export const getTiprackVolume = (labwareDef: LabwareDefinition2): number => {
85
+ assert(
86
+ labwareDef.parameters.isTiprack,
87
+ `getTiprackVolume expected a tiprack labware ${getLabwareDefURI(
88
+ labwareDef
89
+ )}, but 'isTiprack' isn't true`
90
+ )
91
+ // NOTE: Ian 2019-04-16 assuming all tips are the same volume across the rack
92
+ const volume = labwareDef.wells.A1.totalLiquidVolume
93
+ assert(
94
+ volume >= 0,
95
+ `getTiprackVolume expected tip volume to be at least 0, got ${volume}`
96
+ )
97
+ return volume
98
+ }
99
+
100
+ export function getLabwareHasQuirk(
101
+ labwareDef: LabwareDefinition2,
102
+ quirk: string
103
+ ): boolean {
104
+ const quirks = labwareDef.parameters.quirks
105
+ return quirks ? quirks.includes(quirk) : false
106
+ }
107
+
108
+ export const intToAlphabetLetter = (
109
+ i: number,
110
+ lowerCase: boolean = false
111
+ ): string => String.fromCharCode((lowerCase ? 96 : 65) + i)
112
+ // These utils are great candidates for unit tests
113
+ export const toWellName = ({
114
+ rowNum,
115
+ colNum,
116
+ }: {
117
+ rowNum: number
118
+ colNum: number
119
+ }): string => String.fromCharCode(rowNum + 65) + (colNum + 1)
120
+
121
+ function _parseWell(well: string): [string, number] {
122
+ const res = well.match(/([A-Z]+)(\d+)/)
123
+ const letters = res && res[1]
124
+ const number = res && parseInt(res[2])
125
+
126
+ if (!letters || number == null || Number.isNaN(number)) {
127
+ console.warn(
128
+ `Could not parse well ${well}. Got letters: "${
129
+ letters || 'void'
130
+ }", number: "${number || 'void'}"`
131
+ )
132
+ return ['', NaN]
133
+ }
134
+
135
+ return [letters, number]
136
+ }
137
+
138
+ /** A compareFunction for sorting an array of well names
139
+ * Goes down the columns (A1 to H1 on 96 plate)
140
+ * Then L to R across rows (1 to 12 on 96 plate)
141
+ */
142
+ export function sortWells(a: string, b: string): number {
143
+ const [letterA, numberA] = _parseWell(a)
144
+
145
+ const [letterB, numberB] = _parseWell(b)
146
+
147
+ if (numberA !== numberB) {
148
+ return numberA > numberB ? 1 : -1
149
+ }
150
+
151
+ if (letterA.length !== letterB.length) {
152
+ // Eg 'B' vs 'AA'
153
+ return letterA.length > letterB.length ? 1 : -1
154
+ }
155
+
156
+ return letterA > letterB ? 1 : -1
157
+ }
158
+
159
+ export function splitWellsOnColumn(sortedArray: string[]): string[][] {
160
+ return sortedArray.reduce<string[][]>((acc, curr) => {
161
+ const lastColumn = acc.slice(-1)
162
+
163
+ if (lastColumn === undefined || lastColumn.length === 0) {
164
+ return [[curr]]
165
+ }
166
+
167
+ const lastEle = lastColumn[0].slice(-1)[0].slice(1)
168
+
169
+ if (Number(curr.slice(1)) > Number(lastEle)) {
170
+ return [...acc, [curr]]
171
+ } else {
172
+ return [...acc.slice(0, -1), [...lastColumn[0], curr]]
173
+ }
174
+ }, [])
175
+ }
176
+
177
+ export const getWellDepth = (
178
+ labwareDef: LabwareDefinition2,
179
+ well: string
180
+ ): number => labwareDef.wells[well].depth
181
+
182
+ // NOTE: this is used in PD for converting "offset from top" to "mm from bottom".
183
+ // Assumes all wells have same offset because multi-offset not yet supported.
184
+ // TODO: Ian 2019-07-13 return {[string: well]: offset} to support multi-offset
185
+ export const getWellsDepth = (
186
+ labwareDef: LabwareDefinition2,
187
+ wells: string[]
188
+ ): number => {
189
+ const offsets = wells.map(well => getWellDepth(labwareDef, well))
190
+
191
+ if (uniq(offsets).length !== 1) {
192
+ console.warn(
193
+ `expected wells ${JSON.stringify(
194
+ wells
195
+ )} to all have same offset, but they were different. Labware def is ${getLabwareDefURI(
196
+ labwareDef
197
+ )}`
198
+ )
199
+ }
200
+
201
+ return offsets[0]
202
+ }
203
+
204
+ export const getSlotHasMatingSurfaceUnitVector = (
205
+ deckDef: DeckDefinition,
206
+ addressableAreaName: string
207
+ ): boolean => {
208
+ const matingSurfaceUnitVector = deckDef.locations.addressableAreas.find(
209
+ aa => aa.id === addressableAreaName
210
+ )?.matingSurfaceUnitVector
211
+
212
+ return Boolean(matingSurfaceUnitVector)
213
+ }
214
+
215
+ export const getAreSlotsHorizontallyAdjacent = (
216
+ slotNameA?: string | null,
217
+ slotNameB?: string | null
218
+ ): boolean => {
219
+ if (slotNameA == null || slotNameB == null) {
220
+ return false
221
+ }
222
+ const slotANumber = parseInt(slotNameA)
223
+ const slotBNumber = parseInt(slotNameB)
224
+ if (isNaN(slotBNumber) || isNaN(slotANumber)) {
225
+ return false
226
+ }
227
+ const orderedSlots = standardOt2DeckDef.locations.cutouts
228
+ // intentionally not substracting by 1 because trash (slot 12) should not count
229
+ const numSlots = orderedSlots.length
230
+ if (slotBNumber > numSlots || slotANumber > numSlots) {
231
+ return false
232
+ }
233
+ const slotWidth = orderedSlots[1].position[0] - orderedSlots[0].position[0]
234
+ const slotAPosition = orderedSlots[slotANumber - 1].position
235
+ const slotBPosition = orderedSlots[slotBNumber - 1].position
236
+
237
+ const yPositionSlotA = slotAPosition[1]
238
+ const yPositionSlotB = slotBPosition[1]
239
+
240
+ const xPositionSlotA = slotAPosition[0]
241
+ const xPositionSlotB = slotBPosition[0]
242
+
243
+ const areSlotsHorizontallyAdjacent =
244
+ yPositionSlotA === yPositionSlotB &&
245
+ Math.abs(xPositionSlotA - xPositionSlotB) === slotWidth
246
+
247
+ return areSlotsHorizontallyAdjacent
248
+ }
249
+ export const getAreSlotsVerticallyAdjacent = (
250
+ slotNameA?: string | null,
251
+ slotNameB?: string | null
252
+ ): boolean => {
253
+ if (slotNameA == null || slotNameB == null) {
254
+ return false
255
+ }
256
+ const slotANumber = parseInt(slotNameA)
257
+ const slotBNumber = parseInt(slotNameB)
258
+
259
+ if (isNaN(slotBNumber) || isNaN(slotANumber)) {
260
+ return false
261
+ }
262
+ const orderedSlots = standardOt2DeckDef.locations.cutouts
263
+ // intentionally not substracting by 1 because trash (slot 12) should not count
264
+ const numSlots = orderedSlots.length
265
+
266
+ if (slotBNumber > numSlots || slotANumber > numSlots) {
267
+ return false
268
+ }
269
+ // take the y coord of slot 4, and subtact from y coord of slot 1
270
+ const slotHeight = orderedSlots[3].position[1] - orderedSlots[0].position[1]
271
+ const slotAPosition = orderedSlots[slotANumber - 1].position
272
+ const slotBPosition = orderedSlots[slotBNumber - 1].position
273
+
274
+ const yPositionSlotA = slotAPosition[1]
275
+ const yPositionSlotB = slotBPosition[1]
276
+
277
+ const xPositionSlotA = slotAPosition[0]
278
+ const xPositionSlotB = slotBPosition[0]
279
+
280
+ const areSlotsVerticallyAdjacent =
281
+ xPositionSlotA === xPositionSlotB &&
282
+ Math.abs(yPositionSlotA - yPositionSlotB) === slotHeight
283
+
284
+ return areSlotsVerticallyAdjacent
285
+ }
286
+ // TODO(jr, 11/12/23): rename this utility to mention that it
287
+ // is only used in the OT-2, same with getAreSlotsHorizontallyAdjacent
288
+ // and getAreSlotsVerticallyAdjacent
289
+ export const getAreSlotsAdjacent = (
290
+ slotNameA?: string | null,
291
+ slotNameB?: string | null
292
+ ): boolean =>
293
+ getAreSlotsHorizontallyAdjacent(slotNameA, slotNameB) ||
294
+ getAreSlotsVerticallyAdjacent(slotNameA, slotNameB)
295
+
296
+ export const getIsLabwareAboveHeight = (
297
+ labwareDef: LabwareDefinition2,
298
+ height: number
299
+ ): boolean => labwareDef.dimensions.zDimension > height
300
+
301
+ export const getAdapterName = (labwareLoadname: string): ThermalAdapterName => {
302
+ let adapterName: ThermalAdapterName = 'Universal Flat Adapter'
303
+
304
+ if (
305
+ labwareLoadname ===
306
+ 'opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt'
307
+ ) {
308
+ adapterName = 'PCR Adapter'
309
+ } else if (
310
+ labwareLoadname === 'opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep'
311
+ ) {
312
+ adapterName = 'Deep Well Adapter'
313
+ } else if (
314
+ labwareLoadname ===
315
+ 'opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat'
316
+ ) {
317
+ adapterName = '96 Flat Bottom Adapter'
318
+ }
319
+
320
+ return adapterName
321
+ }
322
+
323
+ export const getCalibrationAdapterLoadName = (
324
+ moduleModel: ModuleModel
325
+ ): string | null => {
326
+ switch (moduleModel) {
327
+ case 'heaterShakerModuleV1':
328
+ return 'opentrons_calibration_adapter_heatershaker_module'
329
+ case 'temperatureModuleV2':
330
+ return 'opentrons_calibration_adapter_temperature_module'
331
+ case 'thermocyclerModuleV2':
332
+ return 'opentrons_calibration_adapter_thermocycler_module'
333
+ default:
334
+ console.error(
335
+ `${moduleModel} does not have an associated calibration adapter`
336
+ )
337
+ return null
338
+ }
339
+ }
340
+
341
+ export const getDeckDefFromRobotType = (
342
+ robotType: RobotType
343
+ ): DeckDefinition => {
344
+ // @ts-expect-error imported JSON not playing nice with TS. see https://github.com/microsoft/TypeScript/issues/32063
345
+ return robotType === 'OT-3 Standard'
346
+ ? standardFlexDeckDef
347
+ : standardOt2DeckDef
348
+ }
@@ -0,0 +1,92 @@
1
+ import isEqual from 'lodash/isEqual'
2
+ import uniqWith from 'lodash/uniqWith'
3
+ import round from 'lodash/round'
4
+ import sortedUniq from 'lodash/sortedUniq'
5
+ import uniq from 'lodash/uniq'
6
+ import type {
7
+ LabwareWell,
8
+ LabwareWellShapeProperties,
9
+ LabwareDefinition2,
10
+ LabwareWellGroupMetadata,
11
+ LabwareBrand,
12
+ } from '../types'
13
+
14
+ export interface LabwareWellGroupProperties {
15
+ xOffsetFromLeft: number
16
+ yOffsetFromTop: number
17
+ xSpacing: number | null
18
+ ySpacing: number | null
19
+ wellCount: number
20
+ shape: LabwareWellShapeProperties | null
21
+ depth: number | null
22
+ totalLiquidVolume: number | null
23
+ metadata: LabwareWellGroupMetadata
24
+ brand: LabwareBrand | null
25
+ }
26
+ const ROUNDING_PRECISION = 2
27
+
28
+ export function getUniqueWellProperties(
29
+ definition: LabwareDefinition2
30
+ ): LabwareWellGroupProperties[] {
31
+ const { groups, wells, dimensions } = definition
32
+
33
+ return groups.map(group => {
34
+ const wellProps = group.wells.map(n => wells[n])
35
+
36
+ const wellDepths = wellProps.map<number>(w => w.depth)
37
+ const wellVolumes = wellProps.map<number>(w => w.totalLiquidVolume)
38
+ const wellShapes = wellProps.map<LabwareWellShapeProperties>(
39
+ (well: LabwareWell) =>
40
+ well.shape === 'circular'
41
+ ? { shape: well.shape, diameter: well.diameter }
42
+ : {
43
+ shape: well.shape,
44
+ xDimension: well.xDimension,
45
+ yDimension: well.yDimension,
46
+ }
47
+ )
48
+
49
+ const xStart = wellProps[0]?.x ?? 0
50
+ const yStart = wellProps[0]?.y ?? 0
51
+
52
+ return {
53
+ metadata: group.metadata,
54
+ brand: group.brand || null,
55
+ xSpacing: getSpacingIfUniform(wellProps, 'x'),
56
+ ySpacing: getSpacingIfUniform(wellProps, 'y'),
57
+ xOffsetFromLeft: xStart,
58
+ yOffsetFromTop: round(dimensions.yDimension - yStart, ROUNDING_PRECISION),
59
+ wellCount: wellProps.length,
60
+ depth: getIfConsistent(wellDepths),
61
+ totalLiquidVolume: getIfConsistent(wellVolumes),
62
+ shape: getIfConsistent(wellShapes),
63
+ }
64
+ })
65
+ }
66
+
67
+ export function getIfConsistent<T>(items: T[]): T | null {
68
+ return uniqWith(items, isEqual).length === 1 ? items[0] : null
69
+ }
70
+
71
+ // returning null means "spacing is irregular"; returning 0 means "there is only 1 well along the given axis"
72
+ export function getSpacingIfUniform(
73
+ wells: LabwareWell[],
74
+ axis: 'x' | 'y'
75
+ ): number | null {
76
+ const wellPositions = sortedUniq(uniq(wells.map(well => well[axis])))
77
+ if (wellPositions.length < 2) return 0
78
+
79
+ const initialSpacing = round(
80
+ wellPositions[1] - wellPositions[0],
81
+ ROUNDING_PRECISION
82
+ )
83
+
84
+ for (let i = 2; i < wellPositions.length; i++) {
85
+ const pos = wellPositions[i]
86
+ const prevWellPos = wellPositions[i - 1]
87
+ const spacing = round(pos - prevWellPos, ROUNDING_PRECISION)
88
+ if (spacing !== initialSpacing) return null
89
+ }
90
+
91
+ return Math.abs(initialSpacing)
92
+ }
@@ -0,0 +1,69 @@
1
+ import zipWith from 'lodash/zipWith'
2
+ import uniq from 'lodash/uniq'
3
+ import compact from 'lodash/compact'
4
+ import flatten from 'lodash/flatten'
5
+
6
+ export type WellOrderOption = 'l2r' | 'r2l' | 't2b' | 'b2t'
7
+
8
+ // labware definitions in shared-data have an ordering
9
+ // attribute which is an Array of Arrays of wells. Each inner
10
+ // Array represents a physical column of wells on the labware
11
+ // this function takes that 2d Array of columns and returns
12
+ // the same wells in a 2d Array where each inner Array represents
13
+ // a physical row of wells on the labware.
14
+ // e.g.
15
+ // [ \ [
16
+ // ['A1', 'B1'], === \ ['A1', 'A2']
17
+ // ['A2', 'B2'] === / ['B1', 'B2']
18
+ // ] / ]
19
+ //
20
+ const _orderingColsToRows = (ordering: string[][]): string[][] =>
21
+ zipWith(...ordering, (...col) => compact(uniq(col)))
22
+
23
+ // given a default "ordering" 2d array from labware definitions
24
+ // where each inner array represents a physical column of wells
25
+ // and well order option selections (e.g. 'l2r', 'r2l', 't2b', 'b2t')
26
+ // this function returns a 1d array of wells from the labware
27
+ // ordered by the given traversal technique
28
+ export const orderWells = (
29
+ defaultOrdering: string[][],
30
+ first: WellOrderOption,
31
+ second: WellOrderOption
32
+ ): string[] => {
33
+ let orderedWells: string[][] = []
34
+
35
+ if (first === 't2b') {
36
+ if (second === 'l2r') {
37
+ orderedWells = defaultOrdering
38
+ } else if (second === 'r2l') {
39
+ orderedWells = defaultOrdering.slice().reverse()
40
+ }
41
+ } else if (first === 'b2t') {
42
+ if (second === 'l2r') {
43
+ orderedWells = defaultOrdering.map(col => col.slice().reverse())
44
+ } else if (second === 'r2l') {
45
+ orderedWells = defaultOrdering
46
+ .slice()
47
+ .reverse()
48
+ .map(col => col.slice().reverse())
49
+ }
50
+ } else if (first === 'l2r') {
51
+ if (second === 't2b') {
52
+ orderedWells = _orderingColsToRows(defaultOrdering)
53
+ } else if (second === 'b2t') {
54
+ orderedWells = _orderingColsToRows(defaultOrdering).slice().reverse()
55
+ }
56
+ } else if (first === 'r2l') {
57
+ if (second === 't2b') {
58
+ orderedWells = _orderingColsToRows(defaultOrdering).map(col =>
59
+ col.slice().reverse()
60
+ )
61
+ } else if (second === 'b2t') {
62
+ orderedWells = _orderingColsToRows(defaultOrdering)
63
+ .slice()
64
+ .reverse()
65
+ .map(col => col.slice().reverse())
66
+ }
67
+ }
68
+ return flatten(orderedWells)
69
+ }
@@ -0,0 +1,141 @@
1
+ import Ajv from 'ajv'
2
+ import labwareV2Schema from '../../labware/schemas/2.json'
3
+ import protocolSchemaV1 from '../../protocol/schemas/1.json'
4
+ import protocolSchemaV2 from '../../protocol/schemas/2.json'
5
+ import protocolSchemaV3 from '../../protocol/schemas/3.json'
6
+ import protocolSchemaV4 from '../../protocol/schemas/4.json'
7
+ import protocolSchemaV5 from '../../protocol/schemas/5.json'
8
+
9
+ import type { ErrorObject } from 'ajv'
10
+ import type { JsonProtocolFile } from '../../protocol'
11
+
12
+ export type ProtocolParseErrorKey = 'INVALID_FILE_TYPE' | 'INVALID_JSON_FILE'
13
+
14
+ interface ProtocolParseErrorDetails {
15
+ rawError?: string
16
+ schemaErrors?: ErrorObject[] | null
17
+ }
18
+
19
+ export type ProtocolParseErrorHandler = (
20
+ errorKey: ProtocolParseErrorKey,
21
+ errorDetails?: ProtocolParseErrorDetails
22
+ ) => unknown
23
+
24
+ type ProtocolSchema =
25
+ | typeof protocolSchemaV1
26
+ | typeof protocolSchemaV2
27
+ | typeof protocolSchemaV3
28
+ | typeof protocolSchemaV4
29
+ | typeof protocolSchemaV5
30
+
31
+ const SCHEMA_BY_VERSION: { [version: string]: ProtocolSchema } = {
32
+ '1': protocolSchemaV1,
33
+ '2': protocolSchemaV2,
34
+ '3': protocolSchemaV3,
35
+ '4': protocolSchemaV4,
36
+ '5': protocolSchemaV5,
37
+ }
38
+
39
+ export interface PythonProtocolMetadata {
40
+ source?: string
41
+ [key: string]: unknown
42
+ }
43
+
44
+ // data may be a full JSON protocol or just a metadata dict from Python
45
+ export type ProtocolData =
46
+ | JsonProtocolFile
47
+ | { metadata: PythonProtocolMetadata }
48
+
49
+ export function parseProtocolData(
50
+ file: File,
51
+ contents: string,
52
+ handleError?: ProtocolParseErrorHandler,
53
+ metadata?: PythonProtocolMetadata | null // optional Python protocol metadata
54
+ ): ProtocolData | null {
55
+ if (fileExtensionIsJson(file.name)) {
56
+ return validateJsonProtocolFileContents(contents, handleError)
57
+ } else if (fileExtensionIsPython(file.name)) {
58
+ // grab Python protocol metadata, if any
59
+ return metadata != null ? { metadata } : null
60
+ } else if (fileExtensionIsZip(file.name)) {
61
+ return null
62
+ }
63
+
64
+ handleError && handleError('INVALID_FILE_TYPE')
65
+ return null
66
+ }
67
+
68
+ export function fileExtensionIsPython(filename: string): boolean {
69
+ return Boolean(/\.py$/i.test(filename))
70
+ }
71
+ export function fileExtensionIsJson(filename: string): boolean {
72
+ return Boolean(/\.json$/i.test(filename))
73
+ }
74
+ export function fileExtensionIsZip(filename: string): boolean {
75
+ return Boolean(/\.zip$/i.test(filename))
76
+ }
77
+ export function fileIsBinary(file: File): boolean {
78
+ // bundles are always binary files, and currently nothing else is binary
79
+ return fileExtensionIsZip(file.name)
80
+ }
81
+
82
+ export function validateJsonProtocolFileContents(
83
+ fileContents: string,
84
+ handleError?: ProtocolParseErrorHandler
85
+ ): JsonProtocolFile | null {
86
+ try {
87
+ if (typeof fileContents !== 'string') {
88
+ handleError && handleError('INVALID_FILE_TYPE')
89
+ return null
90
+ } else {
91
+ const ajv = new Ajv({ allErrors: true, jsonPointers: true })
92
+ // protocol schema contains reference to v2 labware schema, so give AJV access to it
93
+ ajv.addSchema(labwareV2Schema)
94
+
95
+ const parsedProtocol = JSON.parse(fileContents) as any
96
+ let validateAgainstSchema
97
+ if ('protocol-schema' in parsedProtocol) {
98
+ // 'protocol-schema' key only present in V1
99
+ validateAgainstSchema = ajv.compile(protocolSchemaV1)
100
+ } else {
101
+ const { schemaVersion } = parsedProtocol
102
+ validateAgainstSchema = ajv.compile(
103
+ SCHEMA_BY_VERSION[String(schemaVersion)]
104
+ )
105
+ }
106
+
107
+ if (!validateAgainstSchema(parsedProtocol)) {
108
+ handleError &&
109
+ handleError('INVALID_JSON_FILE', {
110
+ schemaErrors: validateAgainstSchema.errors,
111
+ })
112
+ return null
113
+ }
114
+
115
+ return parsedProtocol
116
+ }
117
+ } catch (error) {
118
+ handleError && handleError('INVALID_JSON_FILE', { rawError: error })
119
+ return null
120
+ }
121
+ }
122
+
123
+ export function getProtocolDesignerApplicationName(
124
+ protocol: JsonProtocolFile
125
+ ): string | null {
126
+ if (
127
+ 'designerApplication' in protocol &&
128
+ protocol.designerApplication != null
129
+ ) {
130
+ return 'name' in protocol.designerApplication &&
131
+ protocol.designerApplication.name != null
132
+ ? protocol.designerApplication.name
133
+ : null
134
+ } else if (
135
+ 'designer-application' in protocol &&
136
+ protocol['designer-application']['application-name'] != null
137
+ ) {
138
+ return protocol['designer-application']['application-name']
139
+ }
140
+ return null
141
+ }
@@ -0,0 +1,35 @@
1
+ // utilities for working with volumes in µL
2
+ import round from 'lodash/round'
3
+
4
+ import type { LabwareVolumeUnits } from '../types'
5
+
6
+ const SCALE_BY_UNITS = {
7
+ µL: 1,
8
+ mL: 1000,
9
+ L: 1000000,
10
+ }
11
+
12
+ export function getDisplayVolume(
13
+ volumeInMicroliters: number,
14
+ displayUnits: LabwareVolumeUnits = 'µL',
15
+ digits?: number
16
+ ): string {
17
+ const volume = volumeInMicroliters / SCALE_BY_UNITS[displayUnits]
18
+
19
+ return `${typeof digits === 'number' ? round(volume, digits) : volume}`
20
+ }
21
+
22
+ export function getAsciiVolumeUnits(displayUnits: LabwareVolumeUnits): string {
23
+ if (displayUnits === 'µL') return 'uL'
24
+
25
+ return displayUnits
26
+ }
27
+
28
+ export function ensureVolumeUnits(
29
+ maybeUnits: string | null | undefined
30
+ ): LabwareVolumeUnits {
31
+ if (maybeUnits === 'mL' || maybeUnits === 'ml') return 'mL'
32
+ if (maybeUnits === 'L' || maybeUnits === 'l') return 'L'
33
+
34
+ return 'µL'
35
+ }
@@ -0,0 +1,6 @@
1
+ import type { WellDefinition } from '../types'
2
+
3
+ /** Well is either rect or circle. Depends on whether `diameter` exists */
4
+ export function wellIsRect(wellDef: WellDefinition): boolean {
5
+ return !(typeof wellDef.diameter === 'number')
6
+ }