@platforma-open/milaboratories.mixcr-clonotyping-2.workflow 3.26.3 → 3.27.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.26.3 build /home/runner/work/mixcr-clonotyping/mixcr-clonotyping/workflow
3
+ > @platforma-open/milaboratories.mixcr-clonotyping-2.workflow@3.27.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,11 @@
1
1
  # @platforma-open/milaboratories.mixcr-clonotyping.workflow
2
2
 
3
+ ## 3.27.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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".
8
+
3
9
  ## 3.26.3
4
10
 
5
11
  ### 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.0",
4
4
  "description": "Tengo-based template",
5
5
  "dependencies": {
6
6
  "@platforma-sdk/workflow-tengo": "5.12.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.8"
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
@@ -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,