@platforma-open/milaboratories.mixcr-clonotyping-2.workflow 3.26.3 → 3.27.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.
@@ -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.26.3 build /home/runner/work/mixcr-clonotyping/mixcr-clonotyping/workflow
3
+ > @platforma-open/milaboratories.mixcr-clonotyping-2.workflow@3.27.1 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.27.1
4
+
5
+ ### Patch Changes
6
+
7
+ - a297368: Size MiXCR analyze memory from the input reads' file size on top of the per-analysis floor. The per-analysis baseline (64 / 110 / 192 GiB for default / contig-cell / single-cell+MiTool) stays as a floor; memory then grows linearly with the R1+R2 FASTQ blob size (4 bytes of RAM per byte), clamped to 256 GiB. File size is read from blob metadata (getBlobSize) with no pre-exec. The explicit "Advanced Settings" memory override is unchanged; on backends that can't evaluate resource formulas the baseline is the static fallback.
8
+
9
+ ## 3.27.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 1ed23a7: Add "Impute non-covered parts from germline" option for generic amplicon presets. When enabled, the block exports additional germline-imputed sequence columns for the gene-feature regions outside the assembling feature span (plus the full VDJRegion), reconstructing non-covered parts from the assigned V/J germline. Imputed sequences are not used for clonotype assembly. The option is shown only for presets that expose "Assemble clones by".
14
+
3
15
  ## 3.26.3
4
16
 
5
17
  ### Patch Changes
@@ -105,6 +105,71 @@ formatId := func(input) {
105
105
  return result
106
106
  }
107
107
 
108
+
109
+
110
+ germlineRegionOrder := ["FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4"]
111
+
112
+ germlineRegionIndex := func(region) {
113
+ for i, r in germlineRegionOrder {
114
+ if r == region {
115
+ return i
116
+ }
117
+ }
118
+ return -1
119
+ }
120
+
121
+
122
+
123
+
124
+
125
+
126
+
127
+
128
+
129
+ imputedFlankFeatures := func(assembleClonesBy) {
130
+ if is_undefined(assembleClonesBy) || assembleClonesBy == "CDR3" || assembleClonesBy == "VDJRegion" {
131
+ return []
132
+ }
133
+
134
+ begin := undefined
135
+ end := undefined
136
+
137
+ toParts := text.split(assembleClonesBy, "_TO_")
138
+ if len(toParts) == 2 {
139
+ begin = toParts[0]
140
+ end = toParts[1]
141
+ } else if text.has_prefix(assembleClonesBy, "{") && text.has_suffix(assembleClonesBy, "}") {
142
+ inner := assembleClonesBy[1:len(assembleClonesBy) - 1]
143
+ colonParts := text.split(inner, ":")
144
+ if len(colonParts) != 2 {
145
+ return []
146
+ }
147
+ begin = text.replace(colonParts[0], "Begin", "", -1)
148
+ end = text.replace(colonParts[1], "End", "", -1)
149
+ } else {
150
+ return []
151
+ }
152
+
153
+ iBegin := germlineRegionIndex(begin)
154
+ iEnd := germlineRegionIndex(end)
155
+ if iBegin == -1 || iEnd == -1 || iBegin > iEnd {
156
+ return []
157
+ }
158
+
159
+ imputed := []
160
+ for i := 0; i < iBegin; i++ {
161
+ imputed = append(imputed, germlineRegionOrder[i])
162
+ }
163
+ for i := iEnd + 1; i < len(germlineRegionOrder); i++ {
164
+ imputed = append(imputed, germlineRegionOrder[i])
165
+ }
166
+
167
+ if !(begin == "FR1" && end == "FR4") {
168
+ imputed = append(imputed, "VDJRegion")
169
+ }
170
+ return imputed
171
+ }
172
+
108
173
  exportSpecOpsFromPreset := func(presetSpecForBack) {
109
174
  assemblingFeature := undefined
110
175
  if !is_undefined(presetSpecForBack.assemblingFeature) {
@@ -129,7 +194,7 @@ addSpec := func(columns, additionalSpec) {
129
194
 
130
195
 
131
196
 
132
- calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, exportMinQuality) {
197
+ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, exportMinQuality, imputeGermline, assembleClonesBy) {
133
198
  ops := exportSpecOpsFromPreset(presetSpecForBack)
134
199
 
135
200
  assemblingFeature := ops.assemblingFeature
@@ -137,11 +202,14 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
137
202
  cellTags := ops.cellTags
138
203
  hasUmi := ops.hasUmi
139
204
  splitByC := ops.splitByC
140
-
205
+
141
206
 
142
207
  if is_undefined(exportMinQuality) {
143
208
  exportMinQuality = false
144
209
  }
210
+ if is_undefined(imputeGermline) {
211
+ imputeGermline = false
212
+ }
145
213
 
146
214
  isSingleCell := !is_undefined(cellTags) && len(cellTags) > 0
147
215
  hashCellKey := isSingleCell && len(cellTags) > 1
@@ -496,10 +564,40 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
496
564
 
497
565
  annotationTypes := assemblingFeature == "CDR3" ? ["Segments"] : ["CDRs", "Segments"]
498
566
 
499
- for isImputed in ( is_undefined(assemblingFeature) ? [false, true] : [false] ) {
567
+
568
+
569
+
570
+
571
+
572
+
573
+ imputedFeatures := imputeGermline ? imputedFlankFeatures(assembleClonesBy) : []
574
+ doImpute := len(imputedFeatures) > 0
575
+
576
+
577
+
578
+
579
+
580
+
581
+
582
+ if doImpute {
583
+ imputedSet := {}
584
+ for f in imputedFeatures {
585
+ imputedSet[f] = true
586
+ }
587
+ nonImputedFeatures := []
588
+ for f in features {
589
+ if is_undefined(imputedSet[f]) {
590
+ nonImputedFeatures = append(nonImputedFeatures, f)
591
+ }
592
+ }
593
+ features = nonImputedFeatures
594
+ }
595
+
596
+ for isImputed in ( doImpute ? [false, true] : [false] ) {
500
597
  imputedU := isImputed ? "Imputed" : ""
501
598
  imputedL := text.to_lower(imputedU)
502
- for featureU in features {
599
+ featuresList := isImputed ? imputedFeatures : features
600
+ for featureU in featuresList {
503
601
  featureL := text.to_lower(formatId(featureU))
504
602
  for isAminoAcid in [true, false] {
505
603
  featureInFrameU := isAminoAcid ? inFrameFeatures[featureU] : featureU
@@ -539,7 +637,7 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
539
637
  "pl7.app/vdj/isMainSequence": featureU == anchorFeature ? "true" : "false",
540
638
  "pl7.app/vdj/imputed": string(isImputed),
541
639
  "pl7.app/table/fontFamily": "monospace",
542
- "pl7.app/label": featureInFrameU + " " + alphabetShort
640
+ "pl7.app/label": (isImputed ? "Imputed " : "") + featureInFrameU + " " + alphabetShort
543
641
  })
544
642
  }
545
643
  } ]
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.26.3",
3
+ "version": "3.27.1",
4
4
  "description": "Tengo-based template",
5
5
  "dependencies": {
6
- "@platforma-sdk/workflow-tengo": "5.12.0",
6
+ "@platforma-sdk/workflow-tengo": "6.5.0",
7
7
  "@platforma-open/milaboratories.software-mixcr": "4.7.0-347-develop"
8
8
  },
9
9
  "devDependencies": {
10
- "@platforma-sdk/tengo-builder": "3.0.2"
10
+ "@platforma-sdk/tengo-builder": "4.0.9"
11
11
  },
12
12
  "scripts": {
13
13
  "build": "shx rm -rf dist && pl-tengo check && pl-tengo build",
@@ -105,6 +105,71 @@ formatId := func(input) {
105
105
  return result
106
106
  }
107
107
 
108
+ // Gene-feature regions in 5'->3' order, used to derive which regions fall outside an
109
+ // assembled span.
110
+ germlineRegionOrder := ["FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4"]
111
+
112
+ germlineRegionIndex := func(region) {
113
+ for i, r in germlineRegionOrder {
114
+ if r == region {
115
+ return i
116
+ }
117
+ }
118
+ return -1
119
+ }
120
+
121
+ // Derives the gene-feature regions lying OUTSIDE the assembled span (5' and 3' flanks),
122
+ // plus the full VDJRegion, for germline imputation. Mirrors the mixcr-amplicon-alignment
123
+ // block's parseAssemblingFeature, adapted to the "Assemble clones by" value formats:
124
+ // CDR3, VDJRegion -> nothing to impute (minimal / full feature)
125
+ // <X>_TO_<Y> -> span X..Y (e.g. CDR1_TO_FR4)
126
+ // {<X>Begin:<Y>End} -> span X..Y (e.g. {FR1Begin:CDR3End})
127
+ // The assembled span itself is observed, not imputed, so it never gets a duplicate
128
+ // imputed column. Returns [] when there is nothing to impute or the value is unrecognised.
129
+ imputedFlankFeatures := func(assembleClonesBy) {
130
+ if is_undefined(assembleClonesBy) || assembleClonesBy == "CDR3" || assembleClonesBy == "VDJRegion" {
131
+ return []
132
+ }
133
+
134
+ begin := undefined
135
+ end := undefined
136
+
137
+ toParts := text.split(assembleClonesBy, "_TO_")
138
+ if len(toParts) == 2 {
139
+ begin = toParts[0]
140
+ end = toParts[1]
141
+ } else if text.has_prefix(assembleClonesBy, "{") && text.has_suffix(assembleClonesBy, "}") {
142
+ inner := assembleClonesBy[1:len(assembleClonesBy) - 1]
143
+ colonParts := text.split(inner, ":")
144
+ if len(colonParts) != 2 {
145
+ return []
146
+ }
147
+ begin = text.replace(colonParts[0], "Begin", "", -1)
148
+ end = text.replace(colonParts[1], "End", "", -1)
149
+ } else {
150
+ return []
151
+ }
152
+
153
+ iBegin := germlineRegionIndex(begin)
154
+ iEnd := germlineRegionIndex(end)
155
+ if iBegin == -1 || iEnd == -1 || iBegin > iEnd {
156
+ return []
157
+ }
158
+
159
+ imputed := []
160
+ for i := 0; i < iBegin; i++ {
161
+ imputed = append(imputed, germlineRegionOrder[i])
162
+ }
163
+ for i := iEnd + 1; i < len(germlineRegionOrder); i++ {
164
+ imputed = append(imputed, germlineRegionOrder[i])
165
+ }
166
+ // VDJRegion is the full FR1..FR4; impute it unless the span already covers everything.
167
+ if !(begin == "FR1" && end == "FR4") {
168
+ imputed = append(imputed, "VDJRegion")
169
+ }
170
+ return imputed
171
+ }
172
+
108
173
  exportSpecOpsFromPreset := func(presetSpecForBack) {
109
174
  assemblingFeature := undefined
110
175
  if !is_undefined(presetSpecForBack.assemblingFeature) {
@@ -129,7 +194,7 @@ addSpec := func(columns, additionalSpec) {
129
194
  // Ordering rules
130
195
  // AA Sequences
131
196
 
132
- calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, exportMinQuality) {
197
+ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, exportMinQuality, imputeGermline, assembleClonesBy) {
133
198
  ops := exportSpecOpsFromPreset(presetSpecForBack)
134
199
 
135
200
  assemblingFeature := ops.assemblingFeature
@@ -137,11 +202,14 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
137
202
  cellTags := ops.cellTags
138
203
  hasUmi := ops.hasUmi
139
204
  splitByC := ops.splitByC
140
-
205
+
141
206
  // Default to false if not provided
142
207
  if is_undefined(exportMinQuality) {
143
208
  exportMinQuality = false
144
209
  }
210
+ if is_undefined(imputeGermline) {
211
+ imputeGermline = false
212
+ }
145
213
 
146
214
  isSingleCell := !is_undefined(cellTags) && len(cellTags) > 0
147
215
  hashCellKey := isSingleCell && len(cellTags) > 1
@@ -496,10 +564,40 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
496
564
  // aaAnnotationOfSegmentsForVDJRegionInFrame
497
565
  annotationTypes := assemblingFeature == "CDR3" ? ["Segments"] : ["CDRs", "Segments"]
498
566
 
499
- for isImputed in ( is_undefined(assemblingFeature) ? [false, true] : [false] ) {
567
+ // Germline imputation (generic amplicon presets only): when enabled, emit additional
568
+ // germline-imputed sequence columns for the gene-feature regions OUTSIDE the assembled
569
+ // span (plus the full VDJRegion). imputedFlankFeatures returns [] for non-generic presets
570
+ // (assembleClonesBy undefined) and for full/minimal spans, so this never fires outside the
571
+ // intended scope. The clonotype key, built from the non-imputed assembling feature, is
572
+ // unaffected.
573
+ imputedFeatures := imputeGermline ? imputedFlankFeatures(assembleClonesBy) : []
574
+ doImpute := len(imputedFeatures) > 0
575
+
576
+ // Keep the imputed and non-imputed feature sets disjoint (as mixcr-amplicon-alignment does).
577
+ // A flank feature (e.g. FR1) lives in the fixed non-imputed `features` list AND in the imputed
578
+ // set; emitting both yields two columns with identical PColumn identity (name + domain - the
579
+ // `imputed` flag is annotation-only, so it does not disambiguate). The empty non-imputed column
580
+ // (region_not_covered) then shadows the populated germline-imputed one. So when imputing, drop
581
+ // those features from the non-imputed list - only the imputed column remains for the flanks.
582
+ if doImpute {
583
+ imputedSet := {}
584
+ for f in imputedFeatures {
585
+ imputedSet[f] = true
586
+ }
587
+ nonImputedFeatures := []
588
+ for f in features {
589
+ if is_undefined(imputedSet[f]) {
590
+ nonImputedFeatures = append(nonImputedFeatures, f)
591
+ }
592
+ }
593
+ features = nonImputedFeatures
594
+ }
595
+
596
+ for isImputed in ( doImpute ? [false, true] : [false] ) {
500
597
  imputedU := isImputed ? "Imputed" : ""
501
598
  imputedL := text.to_lower(imputedU)
502
- for featureU in features {
599
+ featuresList := isImputed ? imputedFeatures : features
600
+ for featureU in featuresList {
503
601
  featureL := text.to_lower(formatId(featureU))
504
602
  for isAminoAcid in [true, false] {
505
603
  featureInFrameU := isAminoAcid ? inFrameFeatures[featureU] : featureU
@@ -539,7 +637,7 @@ calculateExportSpecs := func(presetSpecForBack, sampleIdAxisSpec, blockId, expor
539
637
  "pl7.app/vdj/isMainSequence": featureU == anchorFeature ? "true" : "false",
540
638
  "pl7.app/vdj/imputed": string(isImputed),
541
639
  "pl7.app/table/fontFamily": "monospace",
542
- "pl7.app/label": featureInFrameU + " " + alphabetShort
640
+ "pl7.app/label": (isImputed ? "Imputed " : "") + featureInFrameU + " " + alphabetShort
543
641
  })
544
642
  }
545
643
  } ]
@@ -134,6 +134,7 @@ wf.body(func(args) {
134
134
  materialType: args.materialType,
135
135
  tagPattern: args.tagPattern,
136
136
  assembleClonesBy: args.assembleClonesBy,
137
+ imputeGermline: args.imputeGermline,
137
138
  exportMinQuality: args.exportMinQuality,
138
139
  stopCodonTypes: args.stopCodonTypes,
139
140
  stopCodonReplacements: args.stopCodonReplacements
@@ -86,18 +86,34 @@ self.body(func(inputs) {
86
86
  mixcrCmdBuilder.cpu(16) // default CPUs number per sample
87
87
  }
88
88
 
89
- // Memory limit per process
89
+ // Memory limit per process.
90
+ // Size analyze RAM from the input reads' file size, on top of a per-analysis
91
+ // floor. size("reads") sums the stored (compressed) blob size of the sample's
92
+ // R1+R2 FASTQ via getBlobSize — a metadata read, no pre-exec — so it tracks
93
+ // total input volume (read count and read length) without decompressing.
94
+ // mem = clamp(baseMemGiB + 4 bytes/byte * size, baseMemGiB, 256 GiB)
95
+ // baseMemGiB is the per-analysis floor (64, or 110/192 for contig/cell and
96
+ // single-cell+MiTool pipelines, which need that headroom regardless of size);
97
+ // the linear term scales with input volume; the 256 GiB cap bounds the largest.
98
+ // An "Advanced Settings" memory override takes precedence; on backends that
99
+ // cannot evaluate resource formulas, memFormula falls back to the baseline.
90
100
  if !is_undefined(perProcessMemGB) {
91
101
  // memory per process was set in "Advanced Settings" block directly
92
102
  mixcrCmdBuilder.mem(string(perProcessMemGB) + "GiB")
93
- } else if hasMiToolSteps {
94
- // allow to use more memory for single cell analysis with mitool pre-
95
- mixcrCmdBuilder.mem("192GiB")
96
- } else if hasAssembleContigs || hasAssembleCells {
97
- // allow to use more memory for single cell analysis
98
- mixcrCmdBuilder.mem("110GiB")
99
103
  } else {
100
- mixcrCmdBuilder.mem("64GiB")
104
+ baseMemGiB := 64
105
+ if hasMiToolSteps {
106
+ baseMemGiB = 192
107
+ } else if hasAssembleContigs || hasAssembleCells {
108
+ baseMemGiB = 110
109
+ }
110
+ f := exec.formula
111
+ mixcrCmdBuilder.memFormula(
112
+ f.clamp(
113
+ f.add(f.gib(baseMemGiB), f.mul(f.size("reads"), 4)),
114
+ f.gib(baseMemGiB),
115
+ f.gib(256)),
116
+ { fallback: string(baseMemGiB) + "GiB" })
101
117
  }
102
118
 
103
119
  if !is_undefined(limitInput) {
@@ -166,7 +182,7 @@ self.body(func(inputs) {
166
182
  inputFile := inputMap["[]"]
167
183
  ll.assert(!is_undefined(inputFile), "unexpected agg group structure")
168
184
  inputFileName := "input." + fileExtension
169
- mixcrCmdBuilder.addFile(inputFileName, inputFile)
185
+ mixcrCmdBuilder.addFile(inputFileName, inputFile, { tag: "reads" })
170
186
  mixcrCmdBuilder.arg(inputFileName)
171
187
  } else if inputDataMeta.keyLength == 1 {
172
188
  ll.assert(aggregationAxesNames == ["pl7.app/sequencing/readIndex"], "unexpected aggregation axes names")
@@ -180,7 +196,7 @@ self.body(func(inputs) {
180
196
  if (r[0] != 'R' && r[0] != "I") || (r[1] != '1' && r[1] != '2') || len(r) != 2 {
181
197
  ll.panic("malformed read index: %v", r)
182
198
  }
183
- mixcrCmdBuilder.addFile("input_" + r + "." + fileExtension, inputFile)
199
+ mixcrCmdBuilder.addFile("input_" + r + "." + fileExtension, inputFile, { tag: "reads" })
184
200
  }
185
201
  mixcrCmdBuilder.arg("input_{{R}}." + fileExtension)
186
202
  } else if inputDataMeta.keyLength == 2 {
@@ -199,7 +215,7 @@ self.body(func(inputs) {
199
215
  if is_undefined(int(lane)) {
200
216
  ll.panic("malformed lane: %v", lane)
201
217
  }
202
- mixcrCmdBuilder.addFile("input_L" + lane + "_" + r + "." + fileExtension, inputFile)
218
+ mixcrCmdBuilder.addFile("input_L" + lane + "_" + r + "." + fileExtension, inputFile, { tag: "reads" })
203
219
  }
204
220
  mixcrCmdBuilder.arg("input_L{{n}}_{{R}}." + fileExtension)
205
221
  } else {
@@ -203,7 +203,7 @@ self.body(func(inputs) {
203
203
  }
204
204
  } ]
205
205
 
206
- exportSpecs := calculateExportSpecs(presetSpecForBack, sampleIdAxisSpec, blockId, params.exportMinQuality)
206
+ exportSpecs := calculateExportSpecs(presetSpecForBack, sampleIdAxisSpec, blockId, params.exportMinQuality, params.imputeGermline, params.assembleClonesBy)
207
207
 
208
208
  columnsSpecPerSample := exportSpecs.columnsSpecPerSample
209
209
  columnsSpecPerSampleSc := exportSpecs.columnsSpecPerSampleSc
@@ -9,7 +9,7 @@ self.defineOutputs("exportSpecs")
9
9
  self.body(func(inputs) {
10
10
  presetSpecForBack := inputs.presetSpecForBack.getDataAsJson()
11
11
 
12
- exportSpecs := calculateExportSpecs(presetSpecForBack, {}, "test", false)
12
+ exportSpecs := calculateExportSpecs(presetSpecForBack, {}, "test", false, false, undefined)
13
13
  exportSpecs = maps.deepMerge(exportSpecs, {
14
14
  axisByClonotypeKeyGen: undefined,
15
15
  axisByScClonotypeKeyGen: undefined,