@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.
- package/js/__tests__/__snapshots__/pipettes.test.ts.snap +5969 -0
- package/js/__tests__/deckSchemas.test.ts +87 -0
- package/js/__tests__/errors.test.js +21 -0
- package/js/__tests__/getAreSlotsAdjacent.test.ts +128 -0
- package/js/__tests__/getWellNamePerMultiTip.test.ts +162 -0
- package/js/__tests__/labwareDefQuirks.test.ts +38 -0
- package/js/__tests__/labwareDefSchemaV1.test.ts +82 -0
- package/js/__tests__/labwareDefSchemaV2.test.ts +298 -0
- package/js/__tests__/moduleAccessors.test.ts +50 -0
- package/js/__tests__/moduleSpecsSchema.test.ts +77 -0
- package/js/__tests__/pipetteSchemaV2.test.ts +107 -0
- package/js/__tests__/pipetteSpecSchemas.test.ts +48 -0
- package/js/__tests__/pipettes.test.ts +58 -0
- package/js/__tests__/protocolSchemaV4.test.ts +137 -0
- package/js/__tests__/protocolSchemaV5.test.ts +137 -0
- package/js/__tests__/protocolSchemaV6.test.ts +132 -0
- package/js/__tests__/protocolSchemaV7.test.ts +64 -0
- package/js/__tests__/protocolValidation.test.ts +83 -0
- package/js/__tests__/sortWells.test.ts +40 -0
- package/js/__tests__/splitWellsOnColumn.test.ts +21 -0
- package/js/__tests__/validateErrors.test.js +34 -0
- package/js/constants.ts +405 -0
- package/js/cypressUtils.ts +35 -0
- package/js/errors.ts +33 -0
- package/js/fixtures.ts +156 -0
- package/js/getLabware.ts +145 -0
- package/js/gripper.ts +29 -0
- package/js/helpers/__tests__/getAdapterName.test.ts +31 -0
- package/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts +12 -0
- package/js/helpers/__tests__/getSimplestFlexDeckConfig.test.ts +176 -0
- package/js/helpers/__tests__/getVectorDifference.test.ts +25 -0
- package/js/helpers/__tests__/getVectorSum.test.ts +21 -0
- package/js/helpers/__tests__/labwareInference.test.ts +84 -0
- package/js/helpers/__tests__/orderWells.test.ts +87 -0
- package/js/helpers/__tests__/parseProtocolData.test.ts +165 -0
- package/js/helpers/__tests__/volume.test.ts +108 -0
- package/js/helpers/__tests__/wellSets.test.ts +352 -0
- package/js/helpers/get96Channel384WellPlateWells.ts +44 -0
- package/js/helpers/getAddressableAreasInProtocol.ts +98 -0
- package/js/helpers/getLoadedLabwareDefinitionsByUri.ts +30 -0
- package/js/helpers/getModuleVizDims.ts +118 -0
- package/js/helpers/getOccludedSlotCountForModule.ts +13 -0
- package/js/helpers/getSimplestFlexDeckConfig.ts +183 -0
- package/js/helpers/getVectorDifference.ts +12 -0
- package/js/helpers/getVectorSum.ts +12 -0
- package/js/helpers/getWellNamePerMultiTip.ts +86 -0
- package/js/helpers/getWellTotalVolume.ts +16 -0
- package/js/helpers/index.ts +348 -0
- package/js/helpers/labwareInference.ts +92 -0
- package/js/helpers/orderWells.ts +69 -0
- package/js/helpers/parseProtocolData.ts +141 -0
- package/js/helpers/volume.ts +35 -0
- package/js/helpers/wellIsRect.ts +6 -0
- package/js/helpers/wellSets.ts +155 -0
- package/js/index.ts +14 -0
- package/js/labwareTools/__tests__/__snapshots__/createIrregularLabware.test.ts.snap +3 -0
- package/js/labwareTools/__tests__/__snapshots__/createLabware.test.ts.snap +3 -0
- package/js/labwareTools/__tests__/createDefaultDisplayName.test.ts +111 -0
- package/js/labwareTools/__tests__/createIrregularLabware.test.ts +226 -0
- package/js/labwareTools/__tests__/createLabware.test.ts +147 -0
- package/js/labwareTools/index.ts +452 -0
- package/js/modules.ts +97 -0
- package/js/pipettes.ts +89 -0
- package/js/protocols.ts +290 -0
- package/js/schema.ts +80 -0
- package/js/scripts/generateDeckLayersFromSVG.js +46 -0
- package/js/titleCase.ts +30 -0
- package/js/types.ts +561 -0
- package/lib/js/__tests__/deckSchemas.test.d.ts +1 -0
- package/lib/js/__tests__/getAreSlotsAdjacent.test.d.ts +1 -0
- package/lib/js/__tests__/getWellNamePerMultiTip.test.d.ts +1 -0
- package/lib/js/__tests__/labwareDefQuirks.test.d.ts +1 -0
- package/lib/js/__tests__/labwareDefSchemaV1.test.d.ts +1 -0
- package/lib/js/__tests__/labwareDefSchemaV2.test.d.ts +1 -0
- package/lib/js/__tests__/moduleAccessors.test.d.ts +1 -0
- package/lib/js/__tests__/moduleSpecsSchema.test.d.ts +1 -0
- package/lib/js/__tests__/pipetteSchemaV2.test.d.ts +1 -0
- package/lib/js/__tests__/pipetteSpecSchemas.test.d.ts +1 -0
- package/lib/js/__tests__/pipettes.test.d.ts +1 -0
- package/lib/js/__tests__/protocolSchemaV4.test.d.ts +1 -0
- package/lib/js/__tests__/protocolSchemaV5.test.d.ts +1 -0
- package/lib/js/__tests__/protocolSchemaV6.test.d.ts +1 -0
- package/lib/js/__tests__/protocolSchemaV7.test.d.ts +1 -0
- package/lib/js/__tests__/protocolValidation.test.d.ts +1 -0
- package/lib/js/__tests__/sortWells.test.d.ts +1 -0
- package/lib/js/__tests__/splitWellsOnColumn.test.d.ts +1 -0
- package/lib/js/constants.d.ts +156 -0
- package/lib/js/cypressUtils.d.ts +1 -0
- package/lib/js/errors.d.ts +13 -0
- package/lib/js/fixtures.d.ts +13 -0
- package/lib/js/getLabware.d.ts +8 -0
- package/lib/js/gripper.d.ts +3 -0
- package/lib/js/helpers/__tests__/getAdapterName.test.d.ts +1 -0
- package/lib/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.d.ts +1 -0
- package/lib/js/helpers/__tests__/getSimplestFlexDeckConfig.test.d.ts +1 -0
- package/lib/js/helpers/__tests__/getVectorDifference.test.d.ts +1 -0
- package/lib/js/helpers/__tests__/getVectorSum.test.d.ts +1 -0
- package/lib/js/helpers/__tests__/labwareInference.test.d.ts +1 -0
- package/lib/js/helpers/__tests__/orderWells.test.d.ts +1 -0
- package/lib/js/helpers/__tests__/parseProtocolData.test.d.ts +1 -0
- package/lib/js/helpers/__tests__/volume.test.d.ts +1 -0
- package/lib/js/helpers/__tests__/wellSets.test.d.ts +1 -0
- package/lib/js/helpers/get96Channel384WellPlateWells.d.ts +1 -0
- package/lib/js/helpers/getAddressableAreasInProtocol.d.ts +4 -0
- package/lib/js/helpers/getLoadedLabwareDefinitionsByUri.d.ts +5 -0
- package/lib/js/helpers/getModuleVizDims.d.ts +16 -0
- package/lib/js/helpers/getOccludedSlotCountForModule.d.ts +2 -0
- package/lib/js/helpers/getSimplestFlexDeckConfig.d.ts +14 -0
- package/lib/js/helpers/getVectorDifference.d.ts +2 -0
- package/lib/js/helpers/getVectorSum.d.ts +2 -0
- package/lib/js/helpers/getWellNamePerMultiTip.d.ts +3 -0
- package/lib/js/helpers/getWellTotalVolume.d.ts +2 -0
- package/lib/js/helpers/index.d.ts +44 -0
- package/lib/js/helpers/labwareInference.d.ts +16 -0
- package/lib/js/helpers/orderWells.d.ts +2 -0
- package/lib/js/helpers/parseProtocolData.d.ts +23 -0
- package/lib/js/helpers/volume.d.ts +4 -0
- package/lib/js/helpers/wellIsRect.d.ts +3 -0
- package/lib/js/helpers/wellSets.d.ts +9 -0
- package/lib/js/index.d.ts +14 -0
- package/lib/js/labwareTools/__tests__/createDefaultDisplayName.test.d.ts +1 -0
- package/lib/js/labwareTools/__tests__/createIrregularLabware.test.d.ts +1 -0
- package/lib/js/labwareTools/__tests__/createLabware.test.d.ts +1 -0
- package/lib/js/labwareTools/index.d.ts +65 -0
- package/lib/js/modules.d.ts +6 -0
- package/lib/js/pipettes.d.ts +13 -0
- package/lib/js/protocols.d.ts +3 -0
- package/lib/js/schema.d.ts +90 -0
- package/lib/js/titleCase.d.ts +1 -0
- package/lib/js/types.d.ts +398 -0
- 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
|
+
}
|