@platforma-open/milaboratories.mixcr-clonotyping-2.workflow 2.0.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.
@@ -0,0 +1,337 @@
1
+ // process
2
+
3
+ self := import("@platforma-sdk/workflow-tengo:tpl")
4
+
5
+ ll := import("@platforma-sdk/workflow-tengo:ll")
6
+ assets := import("@platforma-sdk/workflow-tengo:assets")
7
+ pframes := import("@platforma-sdk/workflow-tengo:pframes")
8
+ smart := import("@platforma-sdk/workflow-tengo:smart")
9
+ slices := import("@platforma-sdk/workflow-tengo:slices")
10
+ file := import("@platforma-sdk/workflow-tengo:file")
11
+ llPFrames := import("@platforma-sdk/workflow-tengo:pframes.ll")
12
+ pSpec := import("@platforma-sdk/workflow-tengo:pframes.spec")
13
+ pUtil := import("@platforma-sdk/workflow-tengo:pframes.util")
14
+ pConstants := import("@platforma-sdk/workflow-tengo:pframes.constants")
15
+
16
+ calculateExportSpecs := import(":calculate-export-specs")
17
+
18
+ json := import("json")
19
+ text := import("text")
20
+
21
+ mixcrAnalyzeTpl := assets.importTemplate(":mixcr-analyze")
22
+ mixcrExportTpl := assets.importTemplate(":mixcr-export")
23
+ aggregateByClonotypeKeyTpl := assets.importTemplate(":aggregate-by-clonotype-key")
24
+
25
+ self.awaitState("InputsLocked")
26
+ self.awaitState("params", "ResourceReady")
27
+ self.awaitState("inputSpec", "ResourceReady")
28
+ self.awaitState("presetSpecForBack", "ResourceReady")
29
+ self.awaitState("presetContent", "ResourceReady")
30
+
31
+ self.body(func(inputs) {
32
+
33
+ inputSpec := inputs.inputSpec
34
+ library := inputs.library
35
+
36
+ preset := inputs.preset
37
+ presetSpecForBack := inputs.presetSpecForBack.getDataAsJson()
38
+ presetContent := inputs.presetContent
39
+
40
+ params := inputs.params
41
+ species := params.species
42
+ chains := params.chains
43
+ limitInput := params.limitInput
44
+ blockId := params.blockId
45
+ presetCommonName := params.presetCommonName
46
+ isLibraryFileGzipped := params.isLibraryFileGzipped
47
+
48
+
49
+ if is_undefined(presetSpecForBack) {
50
+ ll.panic("no presetSpecForBack")
51
+ }
52
+
53
+ reports := []
54
+
55
+ for step in presetSpecForBack.reportTypes {
56
+ if step == "align" || step == "assemble" {
57
+ reports = append(reports, {
58
+ id: step,
59
+ fileJson: "result." + step + ".report.json",
60
+ fileTxt: "result." + step + ".report.txt"
61
+ })
62
+ }
63
+ }
64
+
65
+ hasAssembleContigs := false
66
+ hasAssembleCells := false
67
+ for stage in presetSpecForBack.analysisStages {
68
+ if stage == "assembleContigs" {
69
+ hasAssembleContigs = true
70
+ } else if stage == "assembleCells" {
71
+ hasAssembleCells = true
72
+ }
73
+ }
74
+
75
+ // calculating clns annotations
76
+
77
+ joinOrUndefined := func(arr) {
78
+ if is_undefined(arr) {
79
+ return undefined
80
+ } else {
81
+ return text.join(arr, ",")
82
+ }
83
+ }
84
+
85
+ removeUndefined := func(m) {
86
+ r := {}
87
+ for k, v in m {
88
+ if !is_undefined(v) {
89
+ r[k] = v
90
+ }
91
+ }
92
+ return r;
93
+ }
94
+
95
+ clnsAnnotations := removeUndefined({
96
+ "mixcr.com/assemblingFeature": joinOrUndefined(presetSpecForBack.assemblingFeature),
97
+ "mixcr.com/cellTags": joinOrUndefined(presetSpecForBack.cellTags),
98
+ "mixcr.com/coveredFeaturesOnExport": joinOrUndefined(presetSpecForBack.coveredFeaturesOnExport),
99
+ "mixcr.com/umiTags": joinOrUndefined(presetSpecForBack.umiTags),
100
+ "pl7.app/label": "MiXCR Clonesets"
101
+ })
102
+ if hasAssembleContigs {
103
+ clnsAnnotations["mixcr.com/contigsAssembled"] = "true"
104
+ }
105
+ if hasAssembleCells {
106
+ clnsAnnotations["mixcr.com/cellsAssembled"] = "true"
107
+ }
108
+
109
+ fileExtension := inputSpec.domain["pl7.app/fileExtension"]
110
+
111
+ targetOutputs := [ {
112
+ type: "Resource",
113
+ spec: {
114
+ kind: "PColumn",
115
+ valueType: "File",
116
+ name: "mixcr.com/qc",
117
+ domain: {
118
+ "pl7.app/blockId": blockId
119
+ }
120
+ },
121
+ name: "qc"
122
+ }, {
123
+ type: "Resource",
124
+ spec: {
125
+ kind: "PColumn",
126
+ name: "pl7.app/log",
127
+ domain: {
128
+ "pl7.app/blockId": blockId
129
+ },
130
+ valueType: "Log"
131
+ },
132
+ name: "log"
133
+ }, {
134
+ type: "Resource",
135
+ spec: {
136
+ kind: "PColumn",
137
+ name: "mixcr.com/clns",
138
+ domain: {
139
+ "pl7.app/blockId": blockId
140
+ },
141
+ annotations: clnsAnnotations,
142
+ valueType: "File"
143
+ },
144
+ name: "clns"
145
+ }, {
146
+ type: "ResourceMap",
147
+ name: "reports",
148
+ spec: {
149
+ kind: "PColumn",
150
+ name: "mixcr.com/report",
151
+ domain: {
152
+ "pl7.app/blockId": blockId
153
+ },
154
+ valueType: "File",
155
+ axesSpec: [ {
156
+ type: "String",
157
+ name: "mixcr.com/report/source",
158
+ annotations: {
159
+ "pl7.app/label": "Source MiXCR stage"
160
+ }
161
+ }, {
162
+ type: "String",
163
+ name: "mixcr.com/report/format",
164
+ annotations: {
165
+ "pl7.app/label": "Report format"
166
+ },
167
+ domain: {
168
+ "pl7.app/dense": string(json.encode(["json", "txt"]))
169
+ }
170
+ } ]
171
+ }
172
+ } ]
173
+
174
+ exportSpecs := calculateExportSpecs(presetSpecForBack, blockId)
175
+
176
+ columnsSpecPerSample := exportSpecs.columnsSpecPerSample
177
+ columnsSpecPerClonotype := exportSpecs.columnsSpecPerClonotype
178
+ columnsSpec := exportSpecs.columnsSpec
179
+
180
+ clonotypeKeyColumns := exportSpecs.clonotypeKeyColumns
181
+
182
+ axesByClonotypeId := exportSpecs.axesByClonotypeId
183
+ axesByClonotypeKey := exportSpecs.axesByClonotypeKey
184
+
185
+ exportArgs := exportSpecs.exportArgs
186
+
187
+ mainAbundanceColumn := exportSpecs.mainAbundanceColumn
188
+
189
+ mixcrResults := pframes.processColumn(
190
+ { spec: inputSpec, data: inputs.inputData },
191
+ mixcrAnalyzeTpl,
192
+ targetOutputs,
193
+ {
194
+ aggregate: [{
195
+ name: "pl7.app/sequencing/lane",
196
+ optional: true
197
+ }, {
198
+ name: "pl7.app/sequencing/readIndex",
199
+ optional: true
200
+ }],
201
+ // resulting aggregation axes names will be checked against supported combinations
202
+ // in the body template
203
+ passAggregationAxesNames: true,
204
+
205
+ // will be automatically propagated to all output specs
206
+ traceSteps: [{type: "milaboratories.mixcr-clonotyping", id: blockId, importance: 20, label: "MiXCR " + presetCommonName}],
207
+
208
+ extra: {
209
+ preset: preset,
210
+ params: {
211
+ species: species,
212
+ limitInput: limitInput,
213
+ fileExtension: fileExtension,
214
+ reports: reports,
215
+ isLibraryFileGzipped: isLibraryFileGzipped
216
+ },
217
+ library: library,
218
+ presetContent: presetContent
219
+ }
220
+ }
221
+ )
222
+
223
+ clnsFiles := mixcrResults.output("clns")
224
+
225
+ clonotypes := pframes.pFrameBuilder()
226
+
227
+ for chain in chains {
228
+ exportOutputs := [ {
229
+ type: "Resource",
230
+ spec: {
231
+ kind: "PColumn",
232
+ name: "mixcr.com/clonotypeTable",
233
+ domain: {
234
+ "pl7.app/blockId": blockId
235
+ },
236
+ valueType: "File"
237
+ },
238
+ name: "clonotypeTable",
239
+ path: ["tsv"]
240
+ // }, {
241
+ // type: "Xsv",
242
+ // xsvType: "tsv",
243
+ // settings: {
244
+ // axes: axesByClonotypeId,
245
+ // columns: columnsSpec,
246
+ // storageFormat: "Binary",
247
+ // partitionKeyLength: 0
248
+ // },
249
+ // name: "byCloneId",
250
+ // path: ["tsv"]
251
+ } ]
252
+
253
+ if !is_undefined(axesByClonotypeKey) {
254
+ exportOutputs += [ {
255
+ type: "Xsv",
256
+ xsvType: "tsv",
257
+ settings: {
258
+ axes: axesByClonotypeKey,
259
+ columns: columnsSpecPerSample,
260
+ storageFormat: "Binary",
261
+ partitionKeyLength: 0
262
+ },
263
+ name: "byCloneKeyBySample",
264
+ path: ["tsv"]
265
+ } ]
266
+ }
267
+
268
+ exportResults := pframes.processColumn(
269
+ clnsFiles,
270
+ mixcrExportTpl,
271
+ exportOutputs,
272
+ {
273
+ // will be automatically propagated to all output specs
274
+ traceSteps: [{type: "milaboratories.mixcr-clonotyping.export", id: blockId + "." + chain.name, importance: 80, label: chain.name}],
275
+
276
+ extra: {
277
+ params: {
278
+ chains: chain.mixcrChain,
279
+ clonotypeKeyColumns: clonotypeKeyColumns,
280
+ exportArgs: exportArgs
281
+ }
282
+ }
283
+ }
284
+ )
285
+
286
+ // exportResults.addXsvOutputToBuilder(clonotypes, "byCloneId", "byCloneId/" + chain.name + "/")
287
+ exportResults.addXsvOutputToBuilder(clonotypes, "byCloneKeyBySample", "byCloneKeyBySample/" + chain.name + "/")
288
+
289
+ aggregationOutputs := [ {
290
+ type: "Xsv",
291
+ xsvType: "tsv",
292
+ settings: {
293
+ axes: axesByClonotypeKey,
294
+ columns: columnsSpecPerClonotype,
295
+ storageFormat: "Binary",
296
+ partitionKeyLength: 0
297
+ },
298
+ name: "byCloneKey",
299
+ path: ["tsv"]
300
+ } ]
301
+
302
+ aggregateByCloneKey := pframes.processColumn(
303
+ exportResults.output("clonotypeTable"),
304
+ aggregateByClonotypeKeyTpl,
305
+ aggregationOutputs,
306
+ {
307
+ aggregate: ["pl7.app/sampleId"],
308
+ extra: {
309
+ params: {
310
+ mainAbundanceColumn: mainAbundanceColumn,
311
+ clonotypeColumns: slices.map(columnsSpecPerClonotype, func(col) {
312
+ return col.column
313
+ })
314
+ }
315
+ }
316
+ }
317
+ )
318
+
319
+ aggregateByCloneKey.addXsvOutputToBuilder(clonotypes, "byCloneKey", "byCloneKey/" + chain.name + "/")
320
+ }
321
+
322
+ return {
323
+ "qc.spec": mixcrResults.outputSpec("qc"),
324
+ "qc.data": mixcrResults.outputData("qc"),
325
+
326
+ "logs.spec": mixcrResults.outputSpec("log"),
327
+ "logs.data": mixcrResults.outputData("log"),
328
+
329
+ "reports.spec": mixcrResults.outputSpec("reports"),
330
+ "reports.data": mixcrResults.outputData("reports"),
331
+
332
+ "clns.spec": mixcrResults.outputSpec("clns"),
333
+ "clns.data": mixcrResults.outputData("clns"),
334
+
335
+ clonotypes: clonotypes.build()
336
+ }
337
+ })
@@ -0,0 +1,16 @@
1
+ self := import("@platforma-sdk/workflow-tengo:tpl")
2
+ ll := import("@platforma-sdk/workflow-tengo:ll")
3
+
4
+ calculateExportSpecs := import(":calculate-export-specs")
5
+
6
+ self.defineOutputs("exportSpecs")
7
+
8
+ self.body(func(inputs) {
9
+ presetSpecForBack := inputs.presetSpecForBack.getDataAsJson()
10
+
11
+ exportSpecs := calculateExportSpecs(presetSpecForBack, "test")
12
+
13
+ return {
14
+ exportSpecs: exportSpecs
15
+ }
16
+ })
@@ -0,0 +1,28 @@
1
+ // columns-test
2
+
3
+ self := import("@platforma-sdk/workflow-tengo:tpl")
4
+ assets := import("@platforma-sdk/workflow-tengo:assets")
5
+ render := import("@platforma-sdk/workflow-tengo:render")
6
+
7
+ calculatePresetInfoTpl := assets.importTemplate(":calculate-preset-info")
8
+ columnsCalculateTpl := assets.importTemplate(":test.columns-calculate")
9
+
10
+ self.defineOutputs("conf")
11
+
12
+ self.body(func(inputs) {
13
+ preset := inputs.preset
14
+ params := inputs.params
15
+
16
+ getPreset := render.create(calculatePresetInfoTpl, {
17
+ preset: preset,
18
+ params: params
19
+ })
20
+
21
+ calumnsCalculateResult := render.create(columnsCalculateTpl, {
22
+ presetSpecForBack: getPreset.output("presetSpecForBack")
23
+ })
24
+
25
+ return {
26
+ exportSpecs: calumnsCalculateResult.output("exportSpecs")
27
+ }
28
+ })
@@ -0,0 +1,146 @@
1
+ import { ImportFileHandle } from '@platforma-sdk/model';
2
+ import { awaitStableState, tplTest, ML } from '@platforma-sdk/test';
3
+ import { ExpectStatic } from 'vitest';
4
+
5
+ type Preset =
6
+ | {
7
+ type: 'name';
8
+ name: string;
9
+ }
10
+ | {
11
+ type: 'file';
12
+ file: ImportFileHandle;
13
+ };
14
+
15
+ type Params = {
16
+ species?: string;
17
+ };
18
+
19
+ type TestCase = {
20
+ preset: string;
21
+ species?: string;
22
+ check: (expect: ExpectStatic, config: any) => void;
23
+ };
24
+
25
+ const testCases: TestCase[] = [
26
+ {
27
+ preset: 'milab-human-dna-xcr-7genes-multiplex',
28
+ check: (expect, config) => {
29
+ // console.dir(config, { depth: 5 });
30
+ expect(config.axesByClonotypeId).to.have.lengthOf(1);
31
+ expect(config.axesByClonotypeId.find((c: any) => c.column === 'cloneId')).toBeDefined();
32
+ expect(config.columnsSpec.find((c: any) => c.column === 'readCount')).toBeDefined();
33
+ expect(config.columnsSpec.find((c: any) => c.column === 'readFraction')).toBeDefined();
34
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR3')).toBeDefined();
35
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqCDR3')).toBeDefined();
36
+ }
37
+ },
38
+ {
39
+ preset: '10x-sc-xcr-vdj',
40
+ species: 'human',
41
+ check: (expect, config) => {
42
+ // console.dir(config, { depth: 5 });
43
+ expect(config.axesByClonotypeId).to.have.lengthOf(2);
44
+ expect(config.axesByClonotypeId.find((c: any) => c.column === 'tagValueCELL')).toBeDefined();
45
+ expect(config.axesByClonotypeId.find((c: any) => c.column === 'cloneId')).toBeDefined();
46
+ expect(config.columnsSpec.find((c: any) => c.column === 'cellGroup')).toBeDefined();
47
+ expect(config.columnsSpec.find((c: any) => c.column === 'uniqueMoleculeCount')).toBeDefined();
48
+ expect(config.columnsSpec.find((c: any) => c.column === 'uniqueMoleculeFraction')).toBeDefined();
49
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR1')).toBeDefined();
50
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR1')).toBeDefined();
51
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR2')).toBeDefined();
52
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR2')).toBeDefined();
53
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR3')).toBeDefined();
54
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR3')).toBeDefined();
55
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR4')).toBeDefined();
56
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqFR1')).toBeDefined();
57
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqCDR1')).toBeDefined();
58
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqFR2')).toBeDefined();
59
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqCDR2')).toBeDefined();
60
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqFR3')).toBeDefined();
61
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqCDR3')).toBeDefined();
62
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqFR4')).toBeDefined();
63
+ expect(config.columnsSpec.find((c: any) => c.column === 'topChains')).toBeDefined();
64
+ }
65
+ },
66
+ {
67
+ preset: 'cellecta-human-rna-xcr-umi-drivermap-air',
68
+ check: (expect, config) => {
69
+ // console.dir(config, { depth: 5 });
70
+ expect(config.axesByClonotypeId).to.have.lengthOf(1);
71
+ expect(config.axesByClonotypeId.find((c: any) => c.column === 'cloneId')).toBeDefined();
72
+ expect(config.columnsSpec.find((c: any) => c.column === 'uniqueMoleculeCount')).toBeDefined();
73
+ expect(config.columnsSpec.find((c: any) => c.column === 'uniqueMoleculeFraction')).toBeDefined();
74
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR3')).toBeDefined();
75
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqCDR3')).toBeDefined();
76
+ }
77
+ },
78
+ {
79
+ preset: 'takara-human-rna-bcr-umi-smartseq',
80
+ check: (expect, config) => {
81
+ // console.dir(config, { depth: 5 });
82
+ expect(config.axesByClonotypeId).to.have.lengthOf(1);
83
+ expect(config.axesByClonotypeId.find((c: any) => c.column === 'cloneId')).toBeDefined();
84
+ expect(config.columnsSpec.find((c: any) => c.column === 'uniqueMoleculeCount')).toBeDefined();
85
+ expect(config.columnsSpec.find((c: any) => c.column === 'uniqueMoleculeFraction')).toBeDefined();
86
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR1')).toBeDefined();
87
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR1')).toBeDefined();
88
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR2')).toBeDefined();
89
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR2')).toBeDefined();
90
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR3')).toBeDefined();
91
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR3')).toBeDefined();
92
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqFR4')).toBeDefined();
93
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqFR1')).toBeDefined();
94
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqCDR1')).toBeDefined();
95
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqFR2')).toBeDefined();
96
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqCDR2')).toBeDefined();
97
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqFR3')).toBeDefined();
98
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqCDR3')).toBeDefined();
99
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqFR4')).toBeDefined();
100
+ expect(config.columnsSpec.find((c: any) => c.column === 'isotypePrimary')).toBeDefined();
101
+ }
102
+ },
103
+ {
104
+ preset: 'rna-seq',
105
+ species: 'human',
106
+ check: (expect, config) => {
107
+ // console.dir(config, { depth: 5 });
108
+ expect(config.axesByClonotypeId).to.have.lengthOf(1);
109
+ expect(config.axesByClonotypeId.find((c: any) => c.column === 'cloneId')).toBeDefined();
110
+ expect(config.columnsSpec.find((c: any) => c.column === 'readCount')).toBeDefined();
111
+ expect(config.columnsSpec.find((c: any) => c.column === 'readFraction')).toBeDefined();
112
+ expect(config.columnsSpec.find((c: any) => c.column === 'nSeqCDR3')).toBeDefined();
113
+ expect(config.columnsSpec.find((c: any) => c.column === 'aaSeqCDR3')).toBeDefined();
114
+ }
115
+ },
116
+ {
117
+ preset: 'generic-single-cell-gex',
118
+ species: 'human',
119
+ check: (expect, config) => {
120
+ // console.dir(config, { depth: 5 });
121
+ expect(config.axesByClonotypeId).to.have.lengthOf(1);
122
+ expect(config.columnsSpec.find((c: any) => c.column === 'readCount')).toBeDefined();
123
+ }
124
+ }
125
+ ];
126
+
127
+ tplTest.for(testCases)(
128
+ 'checking preset for $preset',
129
+ { timeout: 30000 },
130
+ async ({ preset, species, check }, { helper, expect }) => {
131
+ const resultC = (
132
+ await helper.renderTemplate(true, 'test.columns.test', ['exportSpecs'], (tx) => {
133
+ return {
134
+ preset: tx.createValue(
135
+ ML.Pl.JsonObject,
136
+ JSON.stringify({ type: 'name', name: preset } satisfies Preset)
137
+ ),
138
+ params: tx.createValue(ML.Pl.JsonObject, JSON.stringify({ species } satisfies Params))
139
+ };
140
+ })
141
+ ).computeOutput('exportSpecs', (c) => c?.getDataAsJson());
142
+ const result = await awaitStableState(resultC, 20000);
143
+ // console.dir(result, { depth: 5 });
144
+ check(expect, result);
145
+ }
146
+ );
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
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
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ watch: false,
6
+ maxConcurrency: 3,
7
+ testTimeout: 10000,
8
+ retry: 2
9
+ }
10
+ });