@platforma-open/milaboratories.top-antibodies.workflow 1.16.0 → 1.17.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.
@@ -90,7 +90,7 @@ findMatchingLinkerIndex := func(colsSpec, linkerColumns) {
90
90
  * @param datasetMainAxisName - Name of the main dataset axis (e.g., clonotype axis)
91
91
  * @param linkerColumns - List of linker columns to match against
92
92
  * @param clusterPropertyIdx - Current cluster property index counter
93
- * @return Map with keys: header, clusterAxisIdx, newClusterPropertyIdx
93
+ * @return Map with keys: isClusterProperty, isLinkerColumn, header, clusterAxisIdx, newClusterPropertyIdx
94
94
  */
95
95
  processRankingColumn := func(colsSpec, datasetMainAxisName, linkerColumns, clusterPropertyIdx) {
96
96
  axesNames := slices.map(colsSpec.axesSpec, func (a) { return a.name})
@@ -100,6 +100,7 @@ processRankingColumn := func(colsSpec, datasetMainAxisName, linkerColumns, clust
100
100
  // This is a clonotype property
101
101
  return {
102
102
  isClusterProperty: false,
103
+ isLinkerColumn: false,
103
104
  header: undefined,
104
105
  clusterAxisIdx: undefined,
105
106
  newClusterPropertyIdx: clusterPropertyIdx
@@ -112,11 +113,14 @@ processRankingColumn := func(colsSpec, datasetMainAxisName, linkerColumns, clust
112
113
  header := ""
113
114
  clusterAxisIdx := undefined
114
115
  newClusterPropertyIdx := clusterPropertyIdx
116
+ isLinkerColumn := false
115
117
 
116
118
  if linkerIdx != undefined {
117
- // This column belongs to a linker - use the linker index
119
+ // This column belongs to a linker - header will be generated by caller
120
+ // to ensure uniqueness across multiple columns from same linker
118
121
  header = "Col_linker." + string(linkerIdx)
119
122
  clusterAxisIdx = linkerIdx
123
+ isLinkerColumn = true
120
124
  } else {
121
125
  // This is a generic cluster property (not associated with any linker)
122
126
  header = "Col_cluster." + string(clusterPropertyIdx)
@@ -126,14 +130,430 @@ processRankingColumn := func(colsSpec, datasetMainAxisName, linkerColumns, clust
126
130
 
127
131
  return {
128
132
  isClusterProperty: true,
133
+ isLinkerColumn: isLinkerColumn,
129
134
  header: header,
130
135
  clusterAxisIdx: clusterAxisIdx,
131
136
  newClusterPropertyIdx: newClusterPropertyIdx
132
137
  }
133
138
  }
134
139
 
140
+ /**
141
+ * Builds sorted linker list in the same order as model.
142
+ *
143
+ * @param columns - PBundle containing all columns
144
+ * @param datasetSpec - Dataset specification with axes
145
+ * @return List of linker columns sorted by axis position
146
+ */
147
+ buildSortedLinkers := func(columns, datasetSpec) {
148
+ allLinkersUnsorted := columns.getColumns("linkers")
149
+
150
+ // Collect linkers by axis position (same iteration order as model)
151
+ sortedLinkers := []
152
+ // First: linkers where clonotypeKey is in SECOND axis
153
+ for col in allLinkersUnsorted {
154
+ if datasetSpec.axesSpec[1].name == col.spec.axesSpec[1].name {
155
+ sortedLinkers = append(sortedLinkers, col)
156
+ }
157
+ }
158
+ // Then: linkers where clonotypeKey is in FIRST axis
159
+ for col in allLinkersUnsorted {
160
+ if datasetSpec.axesSpec[1].name == col.spec.axesSpec[0].name {
161
+ sortedLinkers = append(sortedLinkers, col)
162
+ }
163
+ }
164
+
165
+ return sortedLinkers
166
+ }
167
+
168
+ /**
169
+ * Resolves cluster column reference to header name by matching against sortedLinkers.
170
+ *
171
+ * @param args - Arguments containing clusterColumn
172
+ * @param columns - PBundle containing all columns
173
+ * @param sortedLinkers - List of linker columns in proper order
174
+ * @return Cluster column header string or undefined
175
+ */
176
+ resolveClusterColumnHeader := func(args, columns, sortedLinkers) {
177
+ if is_undefined(args.clusterColumn) {
178
+ return undefined
179
+ }
180
+
181
+ // Get the spec for the selected cluster column
182
+ selectedLinkerSpec := columns.getSpec(args.clusterColumn)
183
+ if is_undefined(selectedLinkerSpec) {
184
+ return undefined
185
+ }
186
+
187
+ // Find the clusterId axis in the selected linker
188
+ selectedClusterIdAxis := undefined
189
+ for axis in selectedLinkerSpec.axesSpec {
190
+ if axis.name == "pl7.app/vdj/clusterId" {
191
+ selectedClusterIdAxis = axis
192
+ break
193
+ }
194
+ }
195
+
196
+ if is_undefined(selectedClusterIdAxis) {
197
+ return undefined
198
+ }
199
+
200
+ // Find matching linker by comparing clusterId axis domains
201
+ for linkerIdx, col in sortedLinkers {
202
+ // Get the clusterId axis from this linker
203
+ for axis in col.spec.axesSpec {
204
+ if axis.name == "pl7.app/vdj/clusterId" {
205
+ // Use clusterAxisDomainsMatch for proper domain comparison
206
+ if clusterAxisDomainsMatch(selectedClusterIdAxis, axis) {
207
+ return "clusterAxis_" + string(linkerIdx) + "_0"
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ return undefined
214
+ }
215
+
216
+ /**
217
+ * Initializes and builds complete clone table with all columns.
218
+ * Handles filters, ranking columns, linkers, cluster sizes, and fallback columns.
219
+ *
220
+ * @param pframes - PFrames import
221
+ * @param columns - PBundle containing all columns
222
+ * @param args - Arguments containing filters, rankingOrder, clusterColumn
223
+ * @param datasetSpec - Dataset specification with axes
224
+ * @return Map with keys: cloneTable, filterMap, rankingMap, sortedLinkers, clusterColumnHeader, addedCols
225
+ */
226
+ initializeCloneTable := func(pframes, columns, args, datasetSpec) {
227
+ // Build clonotype table
228
+ cloneTable := pframes.parquetFileBuilder()
229
+ cloneTable.setAxisHeader(datasetSpec.axesSpec[1], "clonotypeKey")
230
+
231
+ // Build linker list in SAME ORDER as model
232
+ sortedLinkers := buildSortedLinkers(columns, datasetSpec)
233
+
234
+ // Add Filters to table
235
+ addedAxes := []
236
+ filterMap := {}
237
+ rankingMap := {}
238
+ addedCols := false
239
+
240
+ if len(args.filters) > 0 {
241
+ for i, filter in args.filters {
242
+ // we check for value presence and for actual pcolumn (cases where upstream block is deleted)
243
+ if filter.value != undefined && columns.getColumn(filter.value.column).spec != undefined {
244
+ // Columns added here might also be in ranking list, so we add default IDs
245
+ cloneTable.add(columns.getColumn(filter.value.column),
246
+ {header: "Filter_" + string(i), id: "filter_" + string(i)})
247
+ addedCols = true
248
+ // Store reference value and filter type associated to this column
249
+ filterMap["Filter_" + string(i)] = filter.filter
250
+ filterMap["Filter_" + string(i)]["valueType"] = columns.getSpec(filter.value.column).valueType
251
+
252
+ // If column does not have main anchor axis we have to include theirs
253
+ colsSpec := columns.getSpec(filter.value.column)
254
+ axesNames := slices.map(colsSpec.axesSpec, func (a) { return a.name})
255
+ if !slices.hasElement(axesNames, datasetSpec.axesSpec[1].name) {
256
+ for na, ax in colsSpec.axesSpec {
257
+ if ax.name != datasetSpec.axesSpec[1].name {
258
+ cloneTable.setAxisHeader(ax, "cluster_" + string(i) + string(na))
259
+ addedAxes = append(addedAxes, ax.name)
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ // Add ranking columns to table
268
+ clusterPropertyIdx := 0
269
+ clonotypePropertyIdx := 0
270
+ linkerColumnCounters := {} // Track column count per linker index
271
+
272
+ if len(args.rankingOrder) > 0 {
273
+ for i, col in args.rankingOrder {
274
+ // we check for value presence and for actual pcolumn (cases where upstream block is deleted)
275
+ if col.value != undefined && columns.getColumn(col.value.column).spec != undefined {
276
+ // Process the ranking column to determine header and cluster axis
277
+ colsSpec := columns.getSpec(col.value.column)
278
+ // Use sortedLinkers for consistent ordering with model
279
+ result := processRankingColumn(colsSpec, datasetSpec.axesSpec[1].name, sortedLinkers, clusterPropertyIdx)
280
+
281
+ header := ""
282
+ if result.isClusterProperty {
283
+ // Check if this column is from a linker
284
+ if result.isLinkerColumn {
285
+ // Track and use counter for this specific linker
286
+ linkerKey := "linker_" + string(result.clusterAxisIdx)
287
+ if is_undefined(linkerColumnCounters[linkerKey]) {
288
+ linkerColumnCounters[linkerKey] = 0
289
+ }
290
+ counter := linkerColumnCounters[linkerKey]
291
+ header = "Col_linker." + string(result.clusterAxisIdx) + "." + string(counter)
292
+ linkerColumnCounters[linkerKey] = counter + 1
293
+ } else {
294
+ header = result.header
295
+ clusterPropertyIdx = result.newClusterPropertyIdx
296
+ }
297
+
298
+ // Add cluster axis with matching index
299
+ for na, ax in colsSpec.axesSpec {
300
+ if ax.name != datasetSpec.axesSpec[1].name && !slices.hasElement(addedAxes, ax.name) {
301
+ axisHeader := "cluster_" + string(result.clusterAxisIdx)
302
+ cloneTable.setAxisHeader(ax, axisHeader)
303
+ addedAxes = append(addedAxes, ax.name)
304
+ }
305
+ }
306
+ } else {
307
+ header = "Col" + string(clonotypePropertyIdx)
308
+ clonotypePropertyIdx = clonotypePropertyIdx + 1
309
+ }
310
+
311
+ cloneTable.add(columns.getColumn(col.value.column), {header: header})
312
+ addedCols = true
313
+ rankingMap[header] = col.rankingOrder
314
+ }
315
+ }
316
+ }
317
+
318
+ // Get linker columns and add them to the table
319
+ linkerClusterIdAxesWithIdx := []
320
+
321
+ for linkerIdx, col in sortedLinkers {
322
+ clusterIdAxis := undefined
323
+ if datasetSpec.axesSpec[1].name == col.spec.axesSpec[1].name {
324
+ // clonotypeKey is in second axis
325
+ cloneTable.add(col, {header: "linker." + string(linkerIdx)})
326
+ cloneTable.setAxisHeader(col.spec.axesSpec[0], "cluster_" + string(linkerIdx))
327
+ clusterIdAxis = col.spec.axesSpec[0]
328
+ addedCols = true
329
+ } else if datasetSpec.axesSpec[1].name == col.spec.axesSpec[0].name {
330
+ // clonotypeKey is in first axis
331
+ cloneTable.add(col, {header: "linker." + string(linkerIdx)})
332
+ cloneTable.setAxisHeader(col.spec.axesSpec[1], "cluster_" + string(linkerIdx))
333
+ clusterIdAxis = col.spec.axesSpec[1]
334
+ addedCols = true
335
+ }
336
+ // Collect clusterId axes from linker columns to match cluster size columns
337
+ if !is_undefined(clusterIdAxis) && clusterIdAxis.name == "pl7.app/vdj/clusterId" {
338
+ linkerClusterIdAxesWithIdx = append(linkerClusterIdAxesWithIdx, {
339
+ axis: clusterIdAxis,
340
+ linkerIdx: linkerIdx
341
+ })
342
+ }
343
+ }
344
+
345
+ // Add cluster size columns if available, matching linker columns' clusterId axes
346
+ if len(columns.getColumns("clusterSizes")) > 0 {
347
+ for col in columns.getColumns("clusterSizes") {
348
+ // Find the clusterId axis in this cluster size column
349
+ clusterSizeClusterIdAxis := undefined
350
+ for axis in col.spec.axesSpec {
351
+ if axis.name == "pl7.app/vdj/clusterId" {
352
+ clusterSizeClusterIdAxis = axis
353
+ break
354
+ }
355
+ }
356
+
357
+ // Find matching linker index
358
+ matchingLinkerIdx := -1
359
+ if len(linkerClusterIdAxesWithIdx) > 0 && !is_undefined(clusterSizeClusterIdAxis) {
360
+ for entry in linkerClusterIdAxesWithIdx {
361
+ linkerAxis := entry.axis
362
+ // Compare domains - they must match exactly for same clustering run
363
+ if clusterSizeClusterIdAxis.name == linkerAxis.name &&
364
+ clusterSizeClusterIdAxis.type == linkerAxis.type &&
365
+ clusterAxisDomainsMatch(clusterSizeClusterIdAxis, linkerAxis) {
366
+ matchingLinkerIdx = entry.linkerIdx
367
+ break
368
+ }
369
+ }
370
+ }
371
+
372
+ // Only add cluster size columns that match a linker column's clustering run
373
+ if matchingLinkerIdx >= 0 {
374
+ cloneTable.add(col, {header: "clusterSize." + string(matchingLinkerIdx)})
375
+ addedCols = true
376
+ // Add the cluster axis header using matching linker index
377
+ for axisIdx, axis in col.spec.axesSpec {
378
+ if axis.name != datasetSpec.axesSpec[1].name {
379
+ cloneTable.setAxisHeader(axis, "clusterAxis_" + string(matchingLinkerIdx) + "_" + string(axisIdx))
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+
386
+ // Fallback: if no columns added, add at least one CDR3 sequence column
387
+ if !addedCols {
388
+ cdr3Sequences := columns.getColumns("cdr3Sequences")
389
+ if len(cdr3Sequences) > 0 {
390
+ cloneTable.add(cdr3Sequences[0], {header: "cdr3_fallback"})
391
+ addedCols = true
392
+ }
393
+ }
394
+
395
+ // Build the table if we have columns
396
+ builtTable := undefined
397
+ clusterColumnHeader := undefined
398
+ if addedCols {
399
+ cloneTable.mem("16GiB")
400
+ cloneTable.cpu(1)
401
+ builtTable = cloneTable.build()
402
+
403
+ // Resolve clusterColumn ref to header name
404
+ clusterColumnHeader = resolveClusterColumnHeader(args, columns, sortedLinkers)
405
+ }
406
+
407
+ return {
408
+ cloneTable: builtTable,
409
+ filterMap: filterMap,
410
+ rankingMap: rankingMap,
411
+ sortedLinkers: sortedLinkers,
412
+ clusterColumnHeader: clusterColumnHeader,
413
+ addedCols: addedCols
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Creates a header name with chain information for CDR3/gene columns.
419
+ *
420
+ * @param col - Column specification
421
+ * @param baseHeaderName - Base name for the header (e.g., "cdr3Sequence", "vGene", "jGene")
422
+ * @param isSingleCell - Whether the data is single cell
423
+ * @return Header name with chain information
424
+ */
425
+ makeHeaderName := func(col, baseHeaderName, isSingleCell) {
426
+ chainMapping := {
427
+ "IG": { "A": "Heavy", "B": "Light" },
428
+ "TCRAB": { "A": "TRA", "B": "TRB" },
429
+ "TCRGD": { "A": "TRG", "B": "TRD" }
430
+ }
431
+
432
+ if isSingleCell {
433
+ chain := col.spec.domain["pl7.app/vdj/scClonotypeChain"] // e.g., "A", "B"
434
+ receptor := col.spec.axesSpec[0].domain["pl7.app/vdj/receptor"] // e.g., "IG", "TCRAB", "TCRGD"
435
+ chainLabel := chainMapping[receptor][chain]
436
+ return baseHeaderName + "." + chainLabel // e.g., "cdr3Sequence.Heavy"
437
+ } else {
438
+ // For bulk, if chain info is available (e.g. IGH, IGK, IGL)
439
+ chainFromDomain := col.spec.axesSpec[0].domain["pl7.app/vdj/chain"] // e.g. "IGH", "IGK"
440
+ if chainFromDomain != undefined {
441
+ return baseHeaderName + "." + chainFromDomain // e.g., "cdr3Sequence.IGH"
442
+ }
443
+ }
444
+ return baseHeaderName
445
+ }
446
+
447
+ /**
448
+ * Initializes and builds CDR3 sequence table with CDR3 sequences, V genes, and J genes.
449
+ *
450
+ * @param pframes - PFrames import
451
+ * @param columns - PBundle containing all columns
452
+ * @param datasetSpec - Dataset specification with axes
453
+ * @param isSingleCell - Whether the data is single cell
454
+ * @return Built CDR3 sequence table
455
+ */
456
+ initializeCdr3SeqTable := func(pframes, columns, datasetSpec, isSingleCell) {
457
+ cdr3SeqTable := pframes.parquetFileBuilder()
458
+ cdr3SeqTable.setAxisHeader(datasetSpec.axesSpec[1].name, "clonotypeKey")
459
+
460
+ // Process CDR3 sequences
461
+ cdr3Sequences := columns.getColumns("cdr3Sequences")
462
+ for col in cdr3Sequences {
463
+ headerName := makeHeaderName(col, "cdr3Sequence", isSingleCell)
464
+ if isSingleCell {
465
+ if col.spec.domain["pl7.app/vdj/scClonotypeChain/index"] == "primary" {
466
+ cdr3SeqTable.add(col, {header: headerName})
467
+ }
468
+ } else {
469
+ cdr3SeqTable.add(col, {header: headerName})
470
+ }
471
+ }
472
+
473
+ // Process V genes
474
+ vGenes := columns.getColumns("VGenes")
475
+ for col in vGenes {
476
+ headerName := makeHeaderName(col, "vGene", isSingleCell)
477
+ cdr3SeqTable.add(col, {header: headerName})
478
+ }
479
+
480
+ // Process J genes
481
+ jGenes := columns.getColumns("JGenes")
482
+ for col in jGenes {
483
+ headerName := makeHeaderName(col, "jGene", isSingleCell)
484
+ cdr3SeqTable.add(col, {header: headerName})
485
+ }
486
+
487
+ cdr3SeqTable.mem("16GiB")
488
+ cdr3SeqTable.cpu(1)
489
+ return cdr3SeqTable.build()
490
+ }
491
+
492
+ /**
493
+ * Detects bulk chain from sequence columns.
494
+ *
495
+ * @param seqCols - List of sequence columns
496
+ * @return Chain string ("H" for Heavy, "KL" for Light)
497
+ */
498
+ detectBulkChain := func(seqCols) {
499
+ chainDetected := "KL"
500
+ for col in seqCols {
501
+ ch := col.spec.axesSpec[0].domain["pl7.app/vdj/chain"] // e.g., IGHeavy, IGLight
502
+ if ch == "IGHeavy" {
503
+ chainDetected = "H"
504
+ break
505
+ }
506
+ if ch == "IGLight" {
507
+ chainDetected = "KL"
508
+ }
509
+ }
510
+ return chainDetected
511
+ }
512
+
513
+ /**
514
+ * Initializes and builds assembling sequence table with assembling AA sequences.
515
+ *
516
+ * @param pframes - PFrames import
517
+ * @param columns - PBundle containing all columns
518
+ * @param datasetSpec - Dataset specification with axes
519
+ * @param isSingleCell - Whether the data is single cell
520
+ * @return Map with keys: assemSeqTable (built table), bulkChain, seqCols
521
+ */
522
+ initializeAssemSeqTable := func(pframes, columns, datasetSpec, isSingleCell) {
523
+ assemSeqTable := pframes.parquetFileBuilder()
524
+ assemSeqTable.setAxisHeader(datasetSpec.axesSpec[1].name, "clonotypeKey")
525
+
526
+ seqCols := columns.getColumns("assemblingAaSeqs")
527
+ for col in seqCols {
528
+ headerName := makeHeaderName(col, "assemblingFeature", isSingleCell)
529
+ assemSeqTable.add(col, {header: headerName})
530
+ }
531
+
532
+ assemSeqTable.mem("16GiB")
533
+ assemSeqTable.cpu(1)
534
+
535
+ // Detect bulk chain if needed
536
+ bulkChain := undefined
537
+ if !isSingleCell {
538
+ bulkChain = detectBulkChain(seqCols)
539
+ }
540
+
541
+ return {
542
+ assemSeqTable: assemSeqTable.build(),
543
+ bulkChain: bulkChain,
544
+ seqCols: seqCols
545
+ }
546
+ }
547
+
135
548
  export {
136
549
  clusterAxisDomainsMatch: clusterAxisDomainsMatch,
137
550
  findMatchingLinkerIndex: findMatchingLinkerIndex,
138
- processRankingColumn: processRankingColumn
551
+ processRankingColumn: processRankingColumn,
552
+ buildSortedLinkers: buildSortedLinkers,
553
+ resolveClusterColumnHeader: resolveClusterColumnHeader,
554
+ initializeCloneTable: initializeCloneTable,
555
+ makeHeaderName: makeHeaderName,
556
+ initializeCdr3SeqTable: initializeCdr3SeqTable,
557
+ detectBulkChain: detectBulkChain,
558
+ initializeAssemSeqTable: initializeAssemSeqTable
139
559
  }
package/index.d.ts DELETED
@@ -1,4 +0,0 @@
1
- declare type TemplateFromFile = { readonly type: "from-file"; readonly path: string; };
2
- declare type TplName = "main";
3
- declare const Templates: Record<TplName, TemplateFromFile>;
4
- export { Templates };
package/index.js DELETED
@@ -1,3 +0,0 @@
1
- module.exports = { Templates: {
2
- 'main': { type: 'from-file', path: require.resolve('./dist/tengo/tpl/main.plj.gz') }
3
- }}
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es2022",
4
- "module": "commonjs",
5
- "moduleResolution": "node",
6
- "esModuleInterop": true,
7
- "strict": true,
8
- "outDir": "./dist",
9
- "rootDir": "./src",
10
- "sourceMap": true,
11
- "declaration": true
12
- },
13
- "types": [],
14
- "include": ["src/**/*"],
15
- "exclude": ["node_modules", "dist"]
16
- }
package/vitest.config.mts DELETED
@@ -1,9 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- watch: false,
6
- maxConcurrency: 3,
7
- testTimeout: 5000
8
- }
9
- });