@platforma-open/milaboratories.mixcr-clonotyping-2.workflow 3.23.5 → 3.24.0

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.
@@ -1,6 +1,6 @@
1
1
   WARN  Issue while reading "/home/runner/work/mixcr-clonotyping/mixcr-clonotyping/.npmrc". Failed to replace env in config: ${NPMJS_TOKEN}
2
2
 
3
- > @platforma-open/milaboratories.mixcr-clonotyping-2.workflow@3.23.5 build /home/runner/work/mixcr-clonotyping/mixcr-clonotyping/workflow
3
+ > @platforma-open/milaboratories.mixcr-clonotyping-2.workflow@3.24.0 build /home/runner/work/mixcr-clonotyping/mixcr-clonotyping/workflow
4
4
  > shx rm -rf dist && pl-tengo check && pl-tengo build
5
5
 
6
6
  info: Skipping unknown file type: test/columns.test.ts
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @platforma-open/milaboratories.mixcr-clonotyping.workflow
2
2
 
3
+ ## 3.24.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 34dfe5e: Add supporting cells column
8
+
9
+ ## 3.23.6
10
+
11
+ ### Patch Changes
12
+
13
+ - b8a3a50: New mutation columns
14
+
3
15
  ## 3.23.5
4
16
 
5
17
  ### Patch Changes
@@ -462,7 +462,27 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
462
462
  })
463
463
  }
464
464
  } ]
465
- columnsSpecPerClonotypeSc = [ sampleCountColumn, clonotypeLabelColumn ]
465
+ columnsSpecPerClonotypeSc = [
466
+ {
467
+ column: "uniqueCellCountTotal",
468
+ id: "cell-count-total",
469
+ allowNA: false,
470
+ spec: {
471
+ name: "pl7.app/vdj/uniqueCellCountTotal",
472
+ valueType: "Long",
473
+ annotations: a(87120, true, {
474
+ "pl7.app/min": "1",
475
+ "pl7.app/isAbundance": "true",
476
+ "pl7.app/abundance/unit": "cells",
477
+ "pl7.app/abundance/normalized": "false",
478
+ "pl7.app/label": "Supporting Cells",
479
+ "pl7.app/description": "The sum of a clonotype's cell counts across all samples."
480
+ })
481
+ }
482
+ },
483
+ sampleCountColumn,
484
+ clonotypeLabelColumn
485
+ ]
466
486
  }
467
487
 
468
488
  orderP := 80000
@@ -767,6 +787,83 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
767
787
 
768
788
 
769
789
 
790
+ shmMapping := []
791
+
792
+ if assemblingFeature == "VDJRegion" {
793
+ nMutationsFeature := coreGeneFeatures["V"] + "," + coreGeneFeatures["J"]
794
+
795
+ crossRegionMutations := [ {
796
+ exportFlag: "nMutationsCount",
797
+ feature: nMutationsFeature,
798
+ id: "n-mutations-total",
799
+ label: "Nt mutations",
800
+ specName: "pl7.app/vdj/sequence/nMutations",
801
+ rankingOrder: "decreasing",
802
+ outputColumn: "nMutations"
803
+ }, {
804
+ exportFlag: "aaMutationsCount",
805
+ feature: "CDR1,CDR2",
806
+ id: "aa-mutations-cdr",
807
+ label: "AA mutations (CDR)",
808
+ specName: "pl7.app/vdj/sequence/nAAMutationsCDR",
809
+ rankingOrder: "decreasing",
810
+ outputColumn: "nAAMutationsCDR"
811
+ }, {
812
+ exportFlag: "aaMutationsCount",
813
+ feature: "FR1,FR2,FR3,FR4",
814
+ id: "aa-mutations-fwr",
815
+ label: "AA mutations (FWR)",
816
+ specName: "pl7.app/vdj/sequence/nAAMutationsFWR",
817
+ rankingOrder: "increasing",
818
+ outputColumn: "nAAMutationsFWR"
819
+ } ]
820
+
821
+ for col in crossRegionMutations {
822
+ columnsSpecPerClonotypeNoAggregates += [ {
823
+ column: col.exportFlag + col.feature,
824
+ id: col.id,
825
+ allowNA: true,
826
+ naRegex: "region_not_covered",
827
+ spec: {
828
+ valueType: "Int",
829
+ name: col.specName,
830
+ annotations: a(orderP, false, {
831
+ "pl7.app/label": col.label,
832
+ "pl7.app/isScore": "true",
833
+ "pl7.app/score/rankingOrder": col.rankingOrder
834
+ })
835
+ }
836
+ } ]
837
+ exportArgs += [ [ "-" + col.exportFlag, col.feature ] ]
838
+ shmMapping = append(shmMapping, {
839
+ outputColumn: col.outputColumn,
840
+ tsvColumn: col.exportFlag + col.feature
841
+ })
842
+ orderP -= 100
843
+ }
844
+
845
+
846
+ columnsSpecPerClonotypeNoAggregates += [ {
847
+ column: "fractionCDRMutations",
848
+ id: "fraction-cdr-mutations",
849
+ naRegex: "^[a-z_]*$",
850
+ allowNA: true,
851
+ spec: {
852
+ valueType: "Double",
853
+ name: "pl7.app/vdj/sequence/fractionCDRMutations",
854
+ annotations: a(orderP, false, {
855
+ "pl7.app/label": "CDR mutation fraction",
856
+ "pl7.app/isScore": "true",
857
+ "pl7.app/score/rankingOrder": "decreasing",
858
+ "pl7.app/format": ".2f"
859
+ })
860
+ }
861
+ } ]
862
+ orderP -= 100
863
+ }
864
+
865
+
866
+
770
867
  flagColumnVariants := [ {
771
868
  columnPrefix: "isProductive",
772
869
  arg: "-isProductive",
@@ -1042,7 +1139,9 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
1042
1139
  exportArgs: exportArgs,
1043
1140
 
1044
1141
  hashCellKey: hashCellKey,
1045
- cellLinkerColumnSettingsGen: cellLinkerColumnSettingsGen
1142
+ cellLinkerColumnSettingsGen: cellLinkerColumnSettingsGen,
1143
+
1144
+ shmMapping: shmMapping
1046
1145
  }
1047
1146
  }
1048
1147
 
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@platforma-open/milaboratories.mixcr-clonotyping-2.workflow",
3
- "version": "3.23.5",
3
+ "version": "3.24.0",
4
4
  "description": "Tengo-based template",
5
5
  "dependencies": {
6
- "@platforma-sdk/workflow-tengo": "5.8.2",
6
+ "@platforma-sdk/workflow-tengo": "5.9.0",
7
7
  "@platforma-open/milaboratories.software-mixcr": "4.7.0-302-develop"
8
8
  },
9
9
  "devDependencies": {
10
- "@platforma-sdk/tengo-builder": "2.4.18"
10
+ "@platforma-sdk/tengo-builder": "2.4.25"
11
11
  },
12
12
  "scripts": {
13
13
  "build": "shx rm -rf dist && pl-tengo check && pl-tengo build",
@@ -78,11 +78,16 @@ self.body(func(inputs) {
78
78
  }
79
79
 
80
80
  aggExpressions := []
81
+ hasFractionCDRMutations := false
81
82
 
82
83
  for colDef in schemaPerClonotypeNoAggregates {
83
84
  if colDef.column == "clonotypeLabel" || colDef.column == "nLengthTotalAdded" {
84
85
  continue
85
86
  }
87
+ if colDef.column == "fractionCDRMutations" {
88
+ hasFractionCDRMutations = true
89
+ continue
90
+ }
86
91
  aggExpressions = append(aggExpressions,
87
92
  pt.col(colDef.column).maxBy(pt.col(mainAbundanceColumnNormalized)).alias(colDef.column)
88
93
  )
@@ -104,6 +109,24 @@ self.body(func(inputs) {
104
109
  alias("nLengthTotalAdded")
105
110
  )
106
111
 
112
+ // Calculate CDR mutation fraction: CDR / (CDR + FWR), fallback 1.0 when NA or zero denominator
113
+ if hasFractionCDRMutations {
114
+ cdr := "aaMutationsCountCDR1,CDR2"
115
+ fwr := "aaMutationsCountFR1,FR2,FR3,FR4"
116
+ aggregatedDf = aggregatedDf.withColumns(
117
+ pt.when(
118
+ pt.col(cdr).isNotNull().
119
+ and(pt.col(fwr).isNotNull()).
120
+ and(pt.col(cdr).cast("Double").plus(pt.col(fwr).cast("Double")).gt(0.0))
121
+ ).then(
122
+ pt.col(cdr).cast("Double").truediv(
123
+ pt.col(cdr).cast("Double").plus(pt.col(fwr).cast("Double"))
124
+ )
125
+ ).otherwise(pt.lit(1.0)).
126
+ alias("fractionCDRMutations")
127
+ )
128
+ }
129
+
107
130
  aggregatedDf = clonotypeLabel.addClonotypeLabelColumnsPt(aggregatedDf, "clonotypeKey", "clonotypeLabel", pt)
108
131
 
109
132
  aggregatedDf.save("output.tsv")
@@ -462,7 +462,27 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
462
462
  })
463
463
  }
464
464
  } ]
465
- columnsSpecPerClonotypeSc = [ sampleCountColumn, clonotypeLabelColumn ]
465
+ columnsSpecPerClonotypeSc = [
466
+ {
467
+ column: "uniqueCellCountTotal",
468
+ id: "cell-count-total",
469
+ allowNA: false,
470
+ spec: {
471
+ name: "pl7.app/vdj/uniqueCellCountTotal",
472
+ valueType: "Long",
473
+ annotations: a(87120, true, {
474
+ "pl7.app/min": "1",
475
+ "pl7.app/isAbundance": "true",
476
+ "pl7.app/abundance/unit": "cells",
477
+ "pl7.app/abundance/normalized": "false",
478
+ "pl7.app/label": "Supporting Cells",
479
+ "pl7.app/description": "The sum of a clonotype's cell counts across all samples."
480
+ })
481
+ }
482
+ },
483
+ sampleCountColumn,
484
+ clonotypeLabelColumn
485
+ ]
466
486
  }
467
487
 
468
488
  orderP := 80000
@@ -765,6 +785,83 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
765
785
  }
766
786
  }
767
787
 
788
+ // Cross-region SHM mutation counts (only for full VDJRegion assembling)
789
+ // shmMapping: [{ outputColumn, tsvColumn }] for single-cell chain summing
790
+ shmMapping := []
791
+
792
+ if assemblingFeature == "VDJRegion" {
793
+ nMutationsFeature := coreGeneFeatures["V"] + "," + coreGeneFeatures["J"]
794
+
795
+ crossRegionMutations := [ {
796
+ exportFlag: "nMutationsCount",
797
+ feature: nMutationsFeature,
798
+ id: "n-mutations-total",
799
+ label: "Nt mutations",
800
+ specName: "pl7.app/vdj/sequence/nMutations",
801
+ rankingOrder: "decreasing",
802
+ outputColumn: "nMutations"
803
+ }, {
804
+ exportFlag: "aaMutationsCount",
805
+ feature: "CDR1,CDR2",
806
+ id: "aa-mutations-cdr",
807
+ label: "AA mutations (CDR)",
808
+ specName: "pl7.app/vdj/sequence/nAAMutationsCDR",
809
+ rankingOrder: "decreasing",
810
+ outputColumn: "nAAMutationsCDR"
811
+ }, {
812
+ exportFlag: "aaMutationsCount",
813
+ feature: "FR1,FR2,FR3,FR4",
814
+ id: "aa-mutations-fwr",
815
+ label: "AA mutations (FWR)",
816
+ specName: "pl7.app/vdj/sequence/nAAMutationsFWR",
817
+ rankingOrder: "increasing",
818
+ outputColumn: "nAAMutationsFWR"
819
+ } ]
820
+
821
+ for col in crossRegionMutations {
822
+ columnsSpecPerClonotypeNoAggregates += [ {
823
+ column: col.exportFlag + col.feature,
824
+ id: col.id,
825
+ allowNA: true,
826
+ naRegex: "region_not_covered",
827
+ spec: {
828
+ valueType: "Int",
829
+ name: col.specName,
830
+ annotations: a(orderP, false, {
831
+ "pl7.app/label": col.label,
832
+ "pl7.app/isScore": "true",
833
+ "pl7.app/score/rankingOrder": col.rankingOrder
834
+ })
835
+ }
836
+ } ]
837
+ exportArgs += [ [ "-" + col.exportFlag, col.feature ] ]
838
+ shmMapping = append(shmMapping, {
839
+ outputColumn: col.outputColumn,
840
+ tsvColumn: col.exportFlag + col.feature
841
+ })
842
+ orderP -= 100
843
+ }
844
+
845
+ // CDR mutation fraction (computed in aggregate-by-clonotype-key)
846
+ columnsSpecPerClonotypeNoAggregates += [ {
847
+ column: "fractionCDRMutations",
848
+ id: "fraction-cdr-mutations",
849
+ naRegex: "^[a-z_]*$",
850
+ allowNA: true,
851
+ spec: {
852
+ valueType: "Double",
853
+ name: "pl7.app/vdj/sequence/fractionCDRMutations",
854
+ annotations: a(orderP, false, {
855
+ "pl7.app/label": "CDR mutation fraction",
856
+ "pl7.app/isScore": "true",
857
+ "pl7.app/score/rankingOrder": "decreasing",
858
+ "pl7.app/format": ".2f"
859
+ })
860
+ }
861
+ } ]
862
+ orderP -= 100
863
+ }
864
+
768
865
  // Flags: productive, oof, stop codons
769
866
 
770
867
  flagColumnVariants := [ {
@@ -1042,7 +1139,9 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
1042
1139
  exportArgs: exportArgs,
1043
1140
 
1044
1141
  hashCellKey: hashCellKey,
1045
- cellLinkerColumnSettingsGen: cellLinkerColumnSettingsGen
1142
+ cellLinkerColumnSettingsGen: cellLinkerColumnSettingsGen,
1143
+
1144
+ shmMapping: shmMapping
1046
1145
  }
1047
1146
  }
1048
1147
 
@@ -203,7 +203,7 @@ self.body(func(inputs) {
203
203
  // PTabler processing for single-cell TSV output
204
204
  wfSingleCell := pt.workflow().
205
205
  inMediumQueue().
206
- mem(ptMemGB).
206
+ mem(ptMemGB * units.GiB).
207
207
  cpu(2)
208
208
 
209
209
  frameLoadOps := {
@@ -17,7 +17,7 @@ math := import("math")
17
17
 
18
18
  self.defineOutputs("abundanceTsv", "clonotypeTsv",
19
19
  "propertiesAPrimaryTsv", "propertiesASecondaryTsv", "propertiesBPrimaryTsv", "propertiesBSecondaryTsv",
20
- "cellsTsv")
20
+ "cellsTsv", "shmTsv")
21
21
 
22
22
  ptablerSw := assets.importSoftware("@platforma-open/milaboratories.software-ptabler:main")
23
23
 
@@ -37,6 +37,11 @@ self.body(func(inputs) {
37
37
 
38
38
  schemaPerClonotypeNoAggregates := inputs.params.schemaPerClonotypeNoAggregates
39
39
 
40
+ shmMapping := inputs.params.shmMapping
41
+ if is_undefined(shmMapping) {
42
+ shmMapping = []
43
+ }
44
+
40
45
  //
41
46
  // Preprocessing
42
47
  //
@@ -193,7 +198,8 @@ self.body(func(inputs) {
193
198
  clonotypeTableDf := allChainsFilteredDf.groupBy(
194
199
  "scClonotypeKey", "clonotypeKeyA1", "clonotypeKeyA2", "clonotypeKeyB1", "clonotypeKeyB2"
195
200
  ).agg(
196
- pt.col("sampleId").nUnique().alias("sampleCount")
201
+ pt.col("sampleId").nUnique().alias("sampleCount"),
202
+ pt.col("cellKey").count().alias("uniqueCellCountTotal")
197
203
  )
198
204
 
199
205
  clonotypeTableDf = clonotypeLabel.addClonotypeLabelColumnsPt(clonotypeTableDf, "scClonotypeKey", "clonotypeLabel", pt)
@@ -234,7 +240,8 @@ self.body(func(inputs) {
234
240
  { column: "clonotypeKeyA2", type: "String" },
235
241
  { column: "clonotypeKeyB1", type: "String" },
236
242
  { column: "clonotypeKeyB2", type: "String" },
237
- { column: "sampleCount", type: "Int" }
243
+ { column: "sampleCount", type: "Int" },
244
+ { column: "uniqueCellCountTotal", type: "Long" }
238
245
  ]
239
246
 
240
247
  mainClonotypesDf := outputProcessingWf.frame(clonotypeTsv, {
@@ -303,6 +310,96 @@ self.body(func(inputs) {
303
310
 
304
311
  outputProcessingRunResult := outputProcessingWf.run()
305
312
 
313
+ propsAPrimaryFile := outputProcessingRunResult.getFile(chainMappings[0].finalOutFile)
314
+ propsBPrimaryFile := outputProcessingRunResult.getFile(chainMappings[2].finalOutFile)
315
+
316
+ // Sum SHM mutation columns across primary A+B chains
317
+ shmTsv := undefined
318
+ if len(shmMapping) > 0 {
319
+ shmSchema := [{ column: "scClonotypeKey", type: "String" }]
320
+ for m in shmMapping {
321
+ shmSchema = append(shmSchema, { column: m.tsvColumn, type: "String" })
322
+ }
323
+
324
+ shmWf := pt.workflow()
325
+
326
+ propsAShmDf := shmWf.frame(propsAPrimaryFile, {
327
+ xsvType: "tsv",
328
+ schema: shmSchema,
329
+ inferSchema: false
330
+ })
331
+ propsBShmDf := shmWf.frame(propsBPrimaryFile, {
332
+ xsvType: "tsv",
333
+ schema: shmSchema,
334
+ inferSchema: false,
335
+ id: "props_b_shm"
336
+ })
337
+
338
+ // Cast "region_not_covered" to null, then to Int
339
+ castExprs := []
340
+ for m in shmMapping {
341
+ castExprs = append(castExprs,
342
+ pt.when(pt.col(m.tsvColumn).eq("region_not_covered")).
343
+ then(pt.lit(undefined)).
344
+ otherwise(pt.col(m.tsvColumn)).
345
+ cast("Int").
346
+ alias(m.tsvColumn)
347
+ )
348
+ }
349
+ propsAShmDf = propsAShmDf.withColumns(castExprs...)
350
+ propsBShmDf = propsBShmDf.withColumns(castExprs...)
351
+
352
+ //Join A and B chains
353
+ leftCols := []
354
+ rightCols := []
355
+ for m in shmMapping {
356
+ leftCols = append(leftCols, { column: m.tsvColumn, rename: m.outputColumn + "_A" })
357
+ rightCols = append(rightCols, { column: m.tsvColumn, rename: m.outputColumn + "_B" })
358
+ }
359
+
360
+ shmCombinedDf := propsAShmDf.join(propsBShmDf, {
361
+ how: "full",
362
+ on: ["scClonotypeKey"],
363
+ coalesce: true,
364
+ leftColumns: leftCols,
365
+ rightColumns: rightCols
366
+ })
367
+
368
+ // Sum SHM mutation columns across primary A+B chains
369
+ for m in shmMapping {
370
+ shmCombinedDf = shmCombinedDf.withColumns(
371
+ pt.col(m.outputColumn + "_A").plus(pt.col(m.outputColumn + "_B")).
372
+ alias(m.outputColumn)
373
+ )
374
+ }
375
+
376
+ // Calculate CDR mutation fraction
377
+ cdr := "nAAMutationsCDR"
378
+ fwr := "nAAMutationsFWR"
379
+ shmCombinedDf = shmCombinedDf.withColumns(
380
+ pt.when(
381
+ pt.col(cdr).isNotNull().
382
+ and(pt.col(fwr).isNotNull()).
383
+ and(pt.col(cdr).cast("Double").plus(pt.col(fwr).cast("Double")).gt(0.0))
384
+ ).then(
385
+ pt.col(cdr).cast("Double").truediv(
386
+ pt.col(cdr).cast("Double").plus(pt.col(fwr).cast("Double"))
387
+ )
388
+ ).otherwise(pt.lit(1.0)).
389
+ alias("fractionCDRMutations")
390
+ )
391
+
392
+ shmOutputCols := ["scClonotypeKey"]
393
+ for m in shmMapping {
394
+ shmOutputCols = append(shmOutputCols, m.outputColumn)
395
+ }
396
+ shmOutputCols = append(shmOutputCols, "fractionCDRMutations")
397
+ shmCombinedDf.save("shm.tsv", { columns: shmOutputCols, xsvType: "tsv" })
398
+
399
+ shmRunResult := shmWf.run()
400
+ shmTsv = shmRunResult.getFile("shm.tsv")
401
+ }
402
+
306
403
  return {
307
404
  // must have sampleId and scClonotypeKey columns
308
405
  abundanceTsv: abundanceTsv,
@@ -314,9 +411,11 @@ self.body(func(inputs) {
314
411
  cellsTsv: cellsTsv,
315
412
 
316
413
  // must have scClonotypeKey columns
317
- propertiesAPrimaryTsv: outputProcessingRunResult.getFile(chainMappings[0].finalOutFile),
414
+ propertiesAPrimaryTsv: propsAPrimaryFile,
318
415
  propertiesASecondaryTsv: outputProcessingRunResult.getFile(chainMappings[1].finalOutFile),
319
- propertiesBPrimaryTsv: outputProcessingRunResult.getFile(chainMappings[2].finalOutFile),
320
- propertiesBSecondaryTsv: outputProcessingRunResult.getFile(chainMappings[3].finalOutFile)
416
+ propertiesBPrimaryTsv: propsBPrimaryFile,
417
+ propertiesBSecondaryTsv: outputProcessingRunResult.getFile(chainMappings[3].finalOutFile),
418
+
419
+ shmTsv: shmTsv
321
420
  }
322
421
  })
@@ -233,6 +233,8 @@ self.body(func(inputs) {
233
233
 
234
234
  mainAbundanceColumnNormalized := exportSpecs.mainAbundanceColumnNormalized
235
235
  mainAbundanceColumnUnnormalized := exportSpecs.mainAbundanceColumnUnnormalized
236
+
237
+ shmMapping := exportSpecs.shmMapping
236
238
  mainAbundanceColumnNormalizedArgs := exportSpecs.mainAbundanceColumnNormalizedArgs
237
239
  mainAbundanceColumnUnnormalizedArgs := exportSpecs.mainAbundanceColumnUnnormalizedArgs
238
240
 
@@ -566,6 +568,77 @@ self.body(func(inputs) {
566
568
  }
567
569
 
568
570
  if isSingleCell {
571
+ // SHM mutation columns: build filtering set and combined output specs
572
+ hasShm := len(shmMapping) > 0
573
+ shmColumnNames := {}
574
+ shmOutputSpecs := []
575
+ if hasShm {
576
+ for m in shmMapping {
577
+ shmColumnNames[m.tsvColumn] = true
578
+ }
579
+ shmColumnNames["fractionCDRMutations"] = true
580
+
581
+ orderP := 10400
582
+ shmDefs := [ {
583
+ outputColumn: "nMutations",
584
+ label: "Nt mutations",
585
+ specName: "pl7.app/vdj/sequence/nMutations",
586
+ valueType: "Int",
587
+ rankingOrder: "decreasing"
588
+ }, {
589
+ outputColumn: "nAAMutationsCDR",
590
+ label: "AA mutations (CDR)",
591
+ specName: "pl7.app/vdj/sequence/nAAMutationsCDR",
592
+ valueType: "Int",
593
+ rankingOrder: "decreasing"
594
+ }, {
595
+ outputColumn: "nAAMutationsFWR",
596
+ label: "AA mutations (FWR)",
597
+ specName: "pl7.app/vdj/sequence/nAAMutationsFWR",
598
+ valueType: "Int",
599
+ rankingOrder: "increasing"
600
+ } ]
601
+
602
+ for def in shmDefs {
603
+ shmOutputSpecs = append(shmOutputSpecs, {
604
+ column: def.outputColumn,
605
+ id: def.outputColumn,
606
+ allowNA: true,
607
+ naRegex: "^[a-z_]*$",
608
+ spec: {
609
+ valueType: def.valueType,
610
+ name: def.specName,
611
+ annotations: {
612
+ "pl7.app/label": def.label,
613
+ "pl7.app/table/orderPriority": string(orderP),
614
+ "pl7.app/table/visibility": "optional",
615
+ "pl7.app/isScore": "true",
616
+ "pl7.app/score/rankingOrder": def.rankingOrder
617
+ }
618
+ }
619
+ })
620
+ orderP -= 100
621
+ }
622
+
623
+ shmOutputSpecs = append(shmOutputSpecs, {
624
+ column: "fractionCDRMutations",
625
+ id: "fraction-cdr-mutations",
626
+ naRegex: "^[a-z_]*$",
627
+ allowNA: true,
628
+ spec: {
629
+ valueType: "Double",
630
+ name: "pl7.app/vdj/sequence/fractionCDRMutations",
631
+ annotations: {
632
+ "pl7.app/label": "CDR mutation fraction",
633
+ "pl7.app/table/orderPriority": string(orderP),
634
+ "pl7.app/table/visibility": "optional",
635
+ "pl7.app/isScore": "true",
636
+ "pl7.app/score/rankingOrder": "decreasing"
637
+ }
638
+ }
639
+ })
640
+ }
641
+
569
642
  for receptor in receptors {
570
643
  receptorInfo := receptorInfos[receptor]
571
644
 
@@ -627,7 +700,10 @@ self.body(func(inputs) {
627
700
 
628
701
  // Modify column visibility for TCR chains
629
702
  isTCRChain := text.has_prefix(chain, "TCR")
630
- columnsForSingleCell := columnsSpecPerClonotypeNoAggregates
703
+ // Filter out SHM mutation columns (We will generate chain-agnostic columns)
704
+ columnsForSingleCell := hasShm ? slices.filter(columnsSpecPerClonotypeNoAggregates, func(col) {
705
+ return !shmColumnNames[col.column]
706
+ }) : columnsSpecPerClonotypeNoAggregates
631
707
  if isTCRChain {
632
708
  visibilitySettings := {
633
709
  "bestCGene": "optional",
@@ -718,6 +794,23 @@ self.body(func(inputs) {
718
794
  path: ["cellsTsv"]
719
795
  } ]
720
796
 
797
+ if hasShm {
798
+ singleCellOutputs += [ {
799
+ type: "Xsv",
800
+ xsvType: "tsv",
801
+ settings: {
802
+ axes: [ axisByScClonotypeKeyGen(receptor) ],
803
+ columns: shmOutputSpecs,
804
+ storageFormat: "Parquet",
805
+ partitionKeyLength: 0
806
+ },
807
+ mem: "12GiB",
808
+ cpu: 2,
809
+ name: "shmCombined",
810
+ path: ["shmTsv"]
811
+ } ]
812
+ }
813
+
721
814
  chainA := receptorInfo.chains[0]
722
815
  chainB := receptorInfo.chains[1]
723
816
 
@@ -746,7 +839,8 @@ self.body(func(inputs) {
746
839
  params: {
747
840
  mainAbundanceColumn: mainAbundanceColumnUnnormalized,
748
841
  mainIsProductiveColumn: mainIsProductiveColumn,
749
- schemaPerClonotypeNoAggregates: columnsToSchema(columnsSpecPerClonotypeNoAggregates)
842
+ schemaPerClonotypeNoAggregates: columnsToSchema(columnsSpecPerClonotypeNoAggregates),
843
+ shmMapping: shmMapping
750
844
  }
751
845
  }
752
846
  }
@@ -769,6 +863,10 @@ self.body(func(inputs) {
769
863
  singleCellResult.addXsvOutputToBuilder(clonotypes, "propertiesBPrimary", "clonotypeProperties/" + receptor + "/bPrimary/")
770
864
  singleCellResult.addXsvOutputToBuilder(clonotypes, "propertiesBSecondary", "clonotypeProperties/" + receptor + "/bSecondary/")
771
865
 
866
+ if hasShm {
867
+ singleCellResult.addXsvOutputToBuilder(clonotypes, "shmCombined", "clonotypeProperties/" + receptor + "/shmCombined/")
868
+ }
869
+
772
870
  for columnName in singleCellResult.listXsvColumns("cellsLinkerTable") {
773
871
  anonymizedData := singleCellResult.outputData("cellsLinkerTable", columnName)
774
872
  clonotypes.add(
@@ -1,5 +1,5 @@
1
1
  import { ImportFileHandle } from '@platforma-sdk/model';
2
- import { awaitStableState, tplTest, ML } from '@platforma-sdk/test';
2
+ import { awaitStableState, ML, tplTest } from '@platforma-sdk/test';
3
3
  import { ExpectStatic } from 'vitest';
4
4
 
5
5
  type Preset =
@@ -46,6 +46,7 @@ const testCases: TestCase[] = [
46
46
  // expect(config.columnsSpec.find((c: any) => c.column === 'cellGroup')).toBeDefined();
47
47
  expect(config.columnsSpec.find((c: any) => c.column === 'uniqueMoleculeCount')).toBeDefined();
48
48
  expect(config.columnsSpec.find((c: any) => c.column === 'uniqueMoleculeFraction')).toBeDefined();
49
+ expect(config.columnsSpecPerClonotypeSc.find((c: any) => c.column === 'uniqueCellCountTotal')).toBeDefined();
49
50
  expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR1')).toBeDefined();
50
51
  expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR1')).toBeDefined();
51
52
  expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR2')).toBeDefined();