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

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,452 @@
1
+ import Ajv from 'ajv'
2
+ import flatten from 'lodash/flatten'
3
+ import range from 'lodash/range'
4
+ import round from 'lodash/round'
5
+
6
+ import labwareSchema from '../../labware/schemas/2.json'
7
+
8
+ import {
9
+ toWellName,
10
+ sortWells,
11
+ splitWellsOnColumn,
12
+ getDisplayVolume,
13
+ getAsciiVolumeUnits,
14
+ ensureVolumeUnits,
15
+ } from '../helpers/index'
16
+
17
+ import type {
18
+ LabwareDefinition2 as Definition,
19
+ LabwareMetadata as Metadata,
20
+ LabwareDimensions as Dimensions,
21
+ LabwareBrand as Brand,
22
+ LabwareParameters as Params,
23
+ LabwareWell as Well,
24
+ LabwareWellProperties as InputWell,
25
+ LabwareWellMap as WellMap,
26
+ LabwareWellGroup as WellGroup,
27
+ LabwareOffset as Offset,
28
+ LabwareVolumeUnits as VolumeUnits,
29
+ } from '../types'
30
+
31
+ // NOTE: leaving this 'beta' to reduce conflicts with future labware cloud namespaces
32
+ export const DEFAULT_CUSTOM_NAMESPACE = 'custom_beta'
33
+
34
+ const SCHEMA_VERSION = 2
35
+ const DEFAULT_BRAND_NAME = 'generic'
36
+
37
+ interface Cell {
38
+ row: number
39
+ column: number
40
+ }
41
+
42
+ // This represents creating a "range" of well names with step intervals included
43
+ // For example, starting at well "A1" with a column stride of 2 would result in
44
+ // the grid name being ordered as: "A1", "B1"..."A3", "B3"..etc
45
+ interface GridStart {
46
+ rowStart: string
47
+ colStart: string
48
+ rowStride: number
49
+ colStride: number
50
+ }
51
+
52
+ type InputParams = Omit<Params, 'loadName'>
53
+
54
+ type InputWellGroup = Omit<WellGroup, 'wells'>
55
+
56
+ export interface BaseLabwareProps {
57
+ metadata: Metadata
58
+ parameters: InputParams
59
+ dimensions: Dimensions
60
+ brand?: Brand
61
+ version?: number
62
+ namespace?: string
63
+ loadNamePostfix?: string[]
64
+ strict?: boolean | null // If true, throws error on failed validation
65
+ }
66
+
67
+ export interface RegularLabwareProps extends BaseLabwareProps {
68
+ offset: Offset
69
+ grid: Cell
70
+ spacing: Cell
71
+ well: InputWell
72
+ group?: InputWellGroup
73
+ }
74
+
75
+ export interface IrregularLabwareProps extends BaseLabwareProps {
76
+ offset: Offset[]
77
+ grid: Cell[]
78
+ spacing: Cell[]
79
+ well: InputWell[]
80
+ gridStart: GridStart[]
81
+ group?: InputWellGroup[]
82
+ }
83
+
84
+ const ajv = new Ajv({ allErrors: true, jsonPointers: true })
85
+ const validate = ajv.compile(labwareSchema)
86
+
87
+ function validateDefinition(
88
+ definition: Definition,
89
+ strict: boolean | null | undefined = true
90
+ ): Definition {
91
+ const valid = validate(definition)
92
+
93
+ if (!valid) {
94
+ console.error('Definition:', definition)
95
+ console.error('Validation Errors:', validate.errors)
96
+
97
+ if (strict) {
98
+ throw new Error(
99
+ 'Generated labware failed to validate, please check your inputs'
100
+ )
101
+ }
102
+ }
103
+
104
+ return definition
105
+ }
106
+
107
+ export function _irregularWellName(
108
+ rowIdx: number,
109
+ colIdx: number,
110
+ gridStart: GridStart
111
+ ): string {
112
+ const rowNum =
113
+ rowIdx * gridStart.rowStride + gridStart.rowStart.charCodeAt(0) - 65
114
+ const colNum = colIdx * gridStart.colStride + parseInt(gridStart.colStart) - 1
115
+ return toWellName({
116
+ rowNum,
117
+ colNum,
118
+ })
119
+ }
120
+
121
+ export function _calculateWellCoord(
122
+ rowIdx: number,
123
+ colIdx: number,
124
+ spacing: Cell,
125
+ offset: Offset,
126
+ well: InputWell
127
+ ): Well {
128
+ const coords = {
129
+ x: round(colIdx * spacing.column + offset.x, 2),
130
+ y: round(rowIdx * spacing.row + offset.y, 2),
131
+ z: round(offset.z - well.depth, 2),
132
+ }
133
+ // NOTE: Ian 2019-04-16 this silly "if circular" is to make Flow happy
134
+ if (well.shape === 'circular') return { ...well, ...coords }
135
+ return { ...well, ...coords }
136
+ }
137
+
138
+ interface Layout {
139
+ wells: WellMap
140
+ groups: WellGroup[]
141
+ }
142
+
143
+ function determineIrregularLayout(
144
+ grids: Cell[],
145
+ spacing: Cell[],
146
+ offset: Offset[],
147
+ gridStart: GridStart[],
148
+ wells: InputWell[],
149
+ group: InputWellGroup[] = []
150
+ ): Layout {
151
+ return grids.reduce<Layout>(
152
+ (result, gridObj, gridIdx) => {
153
+ const reverseRowIdx = range(gridObj.row - 1, -1)
154
+ const inputGroup = group[gridIdx] || {
155
+ metadata: {},
156
+ }
157
+ const currentGroup: WellGroup = { ...inputGroup, wells: [] }
158
+
159
+ range(gridObj.column).forEach(colIdx => {
160
+ range(gridObj.row).forEach(rowIdx => {
161
+ const wellName = _irregularWellName(
162
+ rowIdx,
163
+ colIdx,
164
+ gridStart[gridIdx]
165
+ )
166
+
167
+ currentGroup.wells.push(wellName)
168
+ result.wells[wellName] = _calculateWellCoord(
169
+ reverseRowIdx[rowIdx],
170
+ colIdx,
171
+ spacing[gridIdx],
172
+ offset[gridIdx],
173
+ wells[gridIdx]
174
+ )
175
+ })
176
+ })
177
+ return {
178
+ wells: result.wells,
179
+ groups: [...result.groups, currentGroup],
180
+ }
181
+ },
182
+ {
183
+ wells: {},
184
+ groups: [],
185
+ }
186
+ )
187
+ }
188
+
189
+ export function _generateIrregularLoadName(args: {
190
+ grid: Cell[]
191
+ well: InputWell[]
192
+ totalWellCount: number
193
+ units: VolumeUnits
194
+ brand: string
195
+ displayCategory: string
196
+ loadNamePostfix?: string[]
197
+ }): string {
198
+ const {
199
+ grid,
200
+ well,
201
+ totalWellCount,
202
+ units,
203
+ brand,
204
+ displayCategory,
205
+ loadNamePostfix = [],
206
+ } = args
207
+ const loadNameUnits = getAsciiVolumeUnits(units)
208
+ const wellComboArray = grid.map((gridObj, gridIdx) => {
209
+ const numWells = gridObj.row * gridObj.column
210
+ const wellVolume = getDisplayVolume(well[gridIdx].totalLiquidVolume, units)
211
+ return `${numWells}x${wellVolume}${loadNameUnits}`
212
+ })
213
+ return joinLoadName([
214
+ brand,
215
+ totalWellCount,
216
+ displayCategory,
217
+ wellComboArray,
218
+ ...loadNamePostfix,
219
+ ])
220
+ }
221
+
222
+ // Decide order of wells for single grid containers
223
+ function determineOrdering(grid: Cell): string[][] {
224
+ const ordering = range(grid.column).map(colNum =>
225
+ range(grid.row).map(rowNum =>
226
+ toWellName({
227
+ rowNum,
228
+ colNum,
229
+ })
230
+ )
231
+ )
232
+ return ordering
233
+ }
234
+
235
+ // Decide order of wells for multi-grid containers
236
+ export function determineIrregularOrdering(wellsArray: string[]): string[][] {
237
+ const sortedArray = wellsArray.sort(sortWells)
238
+ const ordering = splitWellsOnColumn(sortedArray)
239
+ return ordering
240
+ }
241
+
242
+ // Private helper functions to calculate the XYZ coordinates of a give well
243
+ // Will return a nested object of all well objects for a labware
244
+ function calculateCoordinates(
245
+ wellProps: InputWell,
246
+ ordering: string[][],
247
+ spacing: Cell,
248
+ offset: Offset,
249
+ dimensions: Dimensions
250
+ ): WellMap {
251
+ const { yDimension } = dimensions
252
+ return ordering.reduce<WellMap>((wellMap, column, cIndex) => {
253
+ return column.reduce<WellMap>(
254
+ (colWellMap, wellName, rIndex) => ({
255
+ ...colWellMap,
256
+ [wellName]: {
257
+ ...wellProps,
258
+ x: round(cIndex * spacing.column + offset.x, 2),
259
+ y: round(yDimension - offset.y - rIndex * spacing.row, 2),
260
+ z: round(offset.z - wellProps.depth, 2),
261
+ },
262
+ }),
263
+ wellMap
264
+ )
265
+ }, {})
266
+ }
267
+
268
+ function ensureBrand(brand?: Brand): Brand {
269
+ return (
270
+ brand || {
271
+ brand: DEFAULT_BRAND_NAME,
272
+ }
273
+ )
274
+ }
275
+
276
+ // joins the input array with _ to create a name, making sure to lowercase the
277
+ // result and remove all invalid characters (allowed characters: [a-z0-9_.])
278
+ function joinLoadName(
279
+ fragments: Array<string | number | Array<string | number>>
280
+ ): string {
281
+ return flatten(fragments)
282
+ .map(s => String(s).replace(/_/g, ''))
283
+ .join('_')
284
+ .toLowerCase()
285
+ .replace(/[^a-z0-9_.]/g, '')
286
+ }
287
+
288
+ export interface RegularNameProps {
289
+ displayCategory: string
290
+ displayVolumeUnits: VolumeUnits
291
+ gridRows: number
292
+ gridColumns: number
293
+ totalLiquidVolume: number
294
+ brandName?: string
295
+ loadNamePostfix?: string[]
296
+ }
297
+
298
+ export function createRegularLoadName(args: RegularNameProps): string {
299
+ const {
300
+ gridRows,
301
+ gridColumns,
302
+ displayCategory,
303
+ totalLiquidVolume,
304
+ displayVolumeUnits,
305
+ brandName = DEFAULT_BRAND_NAME,
306
+ loadNamePostfix = [],
307
+ } = args
308
+ const numWells = gridRows * gridColumns
309
+ return joinLoadName([
310
+ brandName,
311
+ numWells,
312
+ displayCategory,
313
+ `${getDisplayVolume(
314
+ totalLiquidVolume,
315
+ displayVolumeUnits
316
+ )}${getAsciiVolumeUnits(displayVolumeUnits)}`,
317
+ ...loadNamePostfix,
318
+ ])
319
+ }
320
+
321
+ const capitalize = (_s: string): string => {
322
+ const s = _s.trim()
323
+
324
+ return `${s.slice(0, 1).toUpperCase()}${s.slice(1)}`
325
+ }
326
+
327
+ // TODO: Ian 2019-08-23 consider using this in the labware creation functions instead of manually entering displayName
328
+ export function createDefaultDisplayName(args: RegularNameProps): string {
329
+ const {
330
+ gridRows,
331
+ gridColumns,
332
+ displayCategory,
333
+ totalLiquidVolume,
334
+ displayVolumeUnits,
335
+ brandName = DEFAULT_BRAND_NAME,
336
+ loadNamePostfix = [],
337
+ } = args
338
+ const numWells = gridRows * gridColumns
339
+ return [
340
+ ...brandName.split(' ').map(capitalize),
341
+ numWells,
342
+ capitalize(displayCategory.replace(/([a-z])([A-Z])/g, '$1 $2')),
343
+ getDisplayVolume(totalLiquidVolume, displayVolumeUnits),
344
+ displayVolumeUnits,
345
+ ...loadNamePostfix.map(capitalize),
346
+ ]
347
+ .filter(s => s !== '')
348
+ .join(' ')
349
+ .replace(/\s+/g, ' ')
350
+ .trim()
351
+ }
352
+ // Generator function for labware definitions within a regular grid format
353
+ // e.g. well plates, regular tuberacks (NOT 15_50ml) etc.
354
+ // For further info on these parameters look at labware examples in __tests__
355
+ // or the labware definition schema in labware/schemas/
356
+ export function createRegularLabware(args: RegularLabwareProps): Definition {
357
+ const { offset, dimensions, grid, spacing, well, loadNamePostfix } = args
358
+ const strict = args.strict
359
+ const version = args.version || 1
360
+ const namespace = args.namespace || DEFAULT_CUSTOM_NAMESPACE
361
+ const ordering = determineOrdering(grid)
362
+ const brand = ensureBrand(args.brand)
363
+ const groupBase = args.group || {
364
+ metadata: {},
365
+ }
366
+ const metadata = {
367
+ ...args.metadata,
368
+ displayVolumeUnits: ensureVolumeUnits(args.metadata.displayVolumeUnits),
369
+ }
370
+ const loadName = createRegularLoadName({
371
+ gridColumns: grid.column,
372
+ gridRows: grid.row,
373
+ displayCategory: metadata.displayCategory,
374
+ displayVolumeUnits: metadata.displayVolumeUnits,
375
+ totalLiquidVolume: well.totalLiquidVolume,
376
+ brandName: brand.brand,
377
+ loadNamePostfix,
378
+ })
379
+ return validateDefinition(
380
+ {
381
+ ordering,
382
+ brand,
383
+ metadata,
384
+ dimensions,
385
+ wells: calculateCoordinates(well, ordering, spacing, offset, dimensions),
386
+ groups: [{ ...groupBase, wells: flatten(ordering) }],
387
+ parameters: { ...args.parameters, loadName },
388
+ namespace,
389
+ version,
390
+ schemaVersion: SCHEMA_VERSION,
391
+ cornerOffsetFromSlot: {
392
+ x: 0,
393
+ y: 0,
394
+ z: 0,
395
+ },
396
+ },
397
+ strict
398
+ )
399
+ }
400
+ // Generator function for labware definitions within an irregular grid format
401
+ // e.g. crystallization plates, 15_50ml tuberacks and anything with multiple "grids"
402
+ export function createIrregularLabware(
403
+ args: IrregularLabwareProps
404
+ ): Definition {
405
+ const { offset, dimensions, grid, spacing, well, gridStart, group } = args
406
+ const strict = args.strict
407
+ const namespace = args.namespace || DEFAULT_CUSTOM_NAMESPACE
408
+ const version = args.version || 1
409
+ const { wells, groups } = determineIrregularLayout(
410
+ grid,
411
+ spacing,
412
+ offset,
413
+ gridStart,
414
+ well,
415
+ group
416
+ )
417
+ const brand = ensureBrand(args.brand)
418
+ const metadata = {
419
+ ...args.metadata,
420
+ displayVolumeUnits: ensureVolumeUnits(args.metadata.displayVolumeUnits),
421
+ }
422
+
423
+ const loadName = _generateIrregularLoadName({
424
+ grid,
425
+ well,
426
+ totalWellCount: Object.keys(wells).length,
427
+ units: metadata.displayVolumeUnits,
428
+ displayCategory: metadata.displayCategory,
429
+ brand: brand.brand,
430
+ })
431
+
432
+ return validateDefinition(
433
+ {
434
+ wells,
435
+ groups,
436
+ brand,
437
+ metadata,
438
+ dimensions,
439
+ parameters: { ...args.parameters, loadName, format: 'irregular' },
440
+ ordering: determineIrregularOrdering(Object.keys(wells)),
441
+ namespace,
442
+ version,
443
+ schemaVersion: SCHEMA_VERSION,
444
+ cornerOffsetFromSlot: {
445
+ x: 0,
446
+ y: 0,
447
+ z: 0,
448
+ },
449
+ },
450
+ strict
451
+ )
452
+ }
package/js/modules.ts ADDED
@@ -0,0 +1,97 @@
1
+ import magneticModuleV1 from '../module/definitions/3/magneticModuleV1.json'
2
+ import magneticModuleV2 from '../module/definitions/3/magneticModuleV2.json'
3
+ import temperatureModuleV1 from '../module/definitions/3/temperatureModuleV1.json'
4
+ import temperatureModuleV2 from '../module/definitions/3/temperatureModuleV2.json'
5
+ import thermocyclerModuleV1 from '../module/definitions/3/thermocyclerModuleV1.json'
6
+ import thermocyclerModuleV2 from '../module/definitions/3/thermocyclerModuleV2.json'
7
+ import heaterShakerModuleV1 from '../module/definitions/3/heaterShakerModuleV1.json'
8
+ import magneticBlockV1 from '../module/definitions/3/magneticBlockV1.json'
9
+
10
+ import {
11
+ MAGDECK,
12
+ MAGNETIC_MODULE_V1,
13
+ MAGNETIC_MODULE_V2,
14
+ TEMPDECK,
15
+ TEMPERATURE_MODULE_V1,
16
+ TEMPERATURE_MODULE_V2,
17
+ THERMOCYCLER,
18
+ THERMOCYCLER_MODULE_V1,
19
+ THERMOCYCLER_MODULE_V2,
20
+ HEATERSHAKER_MODULE_V1,
21
+ MAGNETIC_BLOCK_V1,
22
+ } from './constants'
23
+
24
+ import type {
25
+ ModuleModel,
26
+ ModuleModelWithLegacy,
27
+ ModuleType,
28
+ ModuleDefinition,
29
+ } from './types'
30
+
31
+ // TODO(bc, 2021-09-18): generate typescript types directly from JSON schema
32
+ // having to maintain TS types side by side with the schema is a liability
33
+ export const getModuleDef2 = (moduleModel: ModuleModel): ModuleDefinition => {
34
+ switch (moduleModel) {
35
+ case MAGNETIC_MODULE_V1:
36
+ return magneticModuleV1 as ModuleDefinition
37
+
38
+ case MAGNETIC_MODULE_V2:
39
+ return (magneticModuleV2 as unknown) as ModuleDefinition
40
+
41
+ case TEMPERATURE_MODULE_V1:
42
+ return temperatureModuleV1 as ModuleDefinition
43
+
44
+ case TEMPERATURE_MODULE_V2:
45
+ return (temperatureModuleV2 as unknown) as ModuleDefinition
46
+
47
+ case THERMOCYCLER_MODULE_V1:
48
+ return thermocyclerModuleV1 as ModuleDefinition
49
+
50
+ case THERMOCYCLER_MODULE_V2:
51
+ return (thermocyclerModuleV2 as unknown) as ModuleDefinition
52
+
53
+ case HEATERSHAKER_MODULE_V1:
54
+ return (heaterShakerModuleV1 as unknown) as ModuleDefinition
55
+
56
+ case MAGNETIC_BLOCK_V1:
57
+ return (magneticBlockV1 as unknown) as ModuleDefinition
58
+
59
+ default:
60
+ throw new Error(`Invalid module model ${moduleModel as string}`)
61
+ }
62
+ }
63
+
64
+ export function normalizeModuleModel(
65
+ legacyModule: ModuleModelWithLegacy
66
+ ): ModuleModel {
67
+ switch (legacyModule) {
68
+ case TEMPDECK:
69
+ return TEMPERATURE_MODULE_V1
70
+
71
+ case MAGDECK:
72
+ return MAGNETIC_MODULE_V1
73
+
74
+ case THERMOCYCLER:
75
+ return THERMOCYCLER_MODULE_V1
76
+
77
+ default:
78
+ throw new Error(`Invalid legacy module model ${legacyModule as string}`)
79
+ }
80
+ }
81
+
82
+ export function getModuleType(moduleModel: ModuleModel): ModuleType {
83
+ return getModuleDef2(moduleModel).moduleType
84
+ }
85
+
86
+ // use module model (not type!) to get model-specific displayName for UI
87
+ export function getModuleDisplayName(moduleModel: ModuleModel): string {
88
+ return getModuleDef2(moduleModel).displayName
89
+ }
90
+
91
+ export function checkModuleCompatibility(
92
+ modelA: ModuleModel,
93
+ modelB: ModuleModel
94
+ ): boolean {
95
+ const bDef = getModuleDef2(modelB)
96
+ return modelA === modelB || bDef.compatibleWith.includes(modelA)
97
+ }
package/js/pipettes.ts ADDED
@@ -0,0 +1,89 @@
1
+ import pipetteNameSpecs from '../pipette/definitions/1/pipetteNameSpecs.json'
2
+ import pipetteModelSpecs from '../pipette/definitions/1/pipetteModelSpecs.json'
3
+ import { OT3_PIPETTES } from './constants'
4
+
5
+ import type { PipetteNameSpecs, PipetteModelSpecs } from './types'
6
+
7
+ type SortableProps = 'maxVolume' | 'channels'
8
+
9
+ // TODO(mc, 2021-04-30): use these types, pulled directly from the JSON,
10
+ // to simplify return types in this module and possibly remove some `null`s
11
+ export type PipetteName = keyof typeof pipetteNameSpecs
12
+ export type PipetteModel = keyof typeof pipetteModelSpecs.config
13
+
14
+ // models sorted by channels and then volume by default
15
+ const ALL_PIPETTE_NAMES: PipetteName[] = (Object.keys(
16
+ pipetteNameSpecs
17
+ ) as PipetteName[]).sort(comparePipettes(['channels', 'maxVolume']))
18
+
19
+ // use a name like 'p10_single' to get specs true for all models under that name
20
+ export function getPipetteNameSpecs(
21
+ name: PipetteName
22
+ ): PipetteNameSpecs | null {
23
+ const config = pipetteNameSpecs[name] as
24
+ | Omit<PipetteNameSpecs, 'name'>
25
+ | undefined
26
+ return config != null ? { ...config, name } : null
27
+ }
28
+
29
+ // specify a model, eg 'p10_single_v1.3' to get
30
+ // both the name specs + model-specific specs
31
+ // NOTE: this should NEVER be used in PD, which is model-agnostic
32
+ export function getPipetteModelSpecs(
33
+ model: PipetteModel
34
+ ): PipetteModelSpecs | null | undefined {
35
+ const modelSpecificFields = pipetteModelSpecs.config[model]
36
+ const modelFields =
37
+ modelSpecificFields &&
38
+ getPipetteNameSpecs(modelSpecificFields.name as PipetteName)
39
+ return modelFields && { ...modelFields, ...modelSpecificFields, model }
40
+ }
41
+
42
+ export function getAllPipetteNames(...sortBy: SortableProps[]): PipetteName[] {
43
+ const models = [...ALL_PIPETTE_NAMES]
44
+ if (sortBy.length) models.sort(comparePipettes(sortBy))
45
+ return models
46
+ }
47
+
48
+ function comparePipettes(sortBy: SortableProps[]) {
49
+ return (modelA: PipetteName, modelB: PipetteName) => {
50
+ // any cast is because we know these pipettes exist
51
+ const a = getPipetteNameSpecs(modelA) as PipetteNameSpecs
52
+ const b = getPipetteNameSpecs(modelB) as PipetteNameSpecs
53
+ let i
54
+
55
+ for (i = 0; i < sortBy.length; i++) {
56
+ const sortKey = sortBy[i]
57
+ if (a[sortKey] < b[sortKey]) return -1
58
+ if (a[sortKey] > b[sortKey]) return 1
59
+ }
60
+
61
+ return 0
62
+ }
63
+ }
64
+
65
+ export function shouldLevel(specs: PipetteNameSpecs): boolean {
66
+ return specs.displayCategory === 'GEN2' && specs.channels === 8
67
+ }
68
+
69
+ export function isFlexPipette(pipetteName: PipetteName): boolean {
70
+ return (
71
+ OT3_PIPETTES.includes(pipetteName) ||
72
+ getPipetteNameSpecs(pipetteName)?.displayCategory === 'FLEX'
73
+ )
74
+ }
75
+
76
+ export const getIncompatiblePipetteNames = (
77
+ currentPipette: PipetteName
78
+ ): string[] => {
79
+ if (isFlexPipette(currentPipette)) {
80
+ return getAllPipetteNames().filter(pipette => !isFlexPipette(pipette))
81
+ } else if (
82
+ getPipetteNameSpecs(currentPipette)?.displayCategory === 'GEN1' ||
83
+ getPipetteNameSpecs(currentPipette)?.displayCategory === 'GEN2'
84
+ ) {
85
+ return getAllPipetteNames().filter(pipette => isFlexPipette(pipette))
86
+ } else {
87
+ return []
88
+ }
89
+ }