@sjcrh/proteinpaint-server 2.190.2-0 → 2.191.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.
- package/package.json +5 -4
- package/src/app.js +10233 -9619
- package/routes/correlationVolcano.js +0 -112
- package/routes/dataset.js +0 -233
- package/routes/dsdata.js +0 -99
- package/routes/gdc.maf.js +0 -85
- package/routes/gdc.mafBuild.js +0 -115
- package/routes/genesetEnrichment.js +0 -226
- package/routes/grin2.js +0 -541
- package/routes/termdb.DE.js +0 -176
- package/routes/termdb.categories.js +0 -123
- package/routes/termdb.chat.js +0 -203
- package/routes/termdb.cluster.js +0 -456
- package/routes/termdb.config.js +0 -347
package/routes/grin2.js
DELETED
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
import serverconfig from "#src/serverconfig.js";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { run_python } from "@sjcrh/proteinpaint-python";
|
|
4
|
-
import { run_rust } from "@sjcrh/proteinpaint-rust";
|
|
5
|
-
import { mayLog } from "#src/helpers.ts";
|
|
6
|
-
import os from "os";
|
|
7
|
-
import { get_samples } from "#src/termdb.sql.js";
|
|
8
|
-
import { read_file, file_is_readable } from "#src/utils.js";
|
|
9
|
-
import { dtsnvindel, dtcnv, dtfusionrna, dtsv, dt2lesion, optionToDt, formatElapsedTime } from "#shared";
|
|
10
|
-
import { mayFilterByMaf } from "#src/mds3.init.js";
|
|
11
|
-
import { cacheOrRecompute } from "#src/utils/cacheOrRecompute.ts";
|
|
12
|
-
import { promisify } from "node:util";
|
|
13
|
-
import { exec as execCallback } from "node:child_process";
|
|
14
|
-
const MAX_LESIONS = serverconfig.features.grin2maxLesions || 25e4;
|
|
15
|
-
const GRIN2_MEMORY_BUDGET_MB = 950;
|
|
16
|
-
const GRIN2_CONCURRENCY_LIMIT = 5;
|
|
17
|
-
const MEMORY_BASE_MB = 260;
|
|
18
|
-
const MEMORY_PER_1K_LESIONS = 2.4;
|
|
19
|
-
const MIN_LESIONS = 5e4;
|
|
20
|
-
function init({ genomes }) {
|
|
21
|
-
return async (req, res) => {
|
|
22
|
-
const signal = req.query.__abortSignal;
|
|
23
|
-
try {
|
|
24
|
-
const request = req.query;
|
|
25
|
-
const g = genomes[request.genome];
|
|
26
|
-
if (!g) throw new Error("genome missing");
|
|
27
|
-
const ds = g.datasets?.[request.dslabel];
|
|
28
|
-
if (!ds) throw new Error("ds missing");
|
|
29
|
-
if (!ds.queries?.singleSampleMutation) throw new Error("singleSampleMutation query missing from dataset");
|
|
30
|
-
const result = await runGrin2WithLimit(g, ds, request, signal);
|
|
31
|
-
res.json(result);
|
|
32
|
-
} catch (e) {
|
|
33
|
-
if (signal?.aborted) {
|
|
34
|
-
mayLog("[GRIN2] Analysis aborted due to client disconnect");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
console.error("[GRIN2] Error stack:", e.stack);
|
|
38
|
-
const errorResponse = {
|
|
39
|
-
status: "error",
|
|
40
|
-
error: e.message || String(e),
|
|
41
|
-
code: e.code
|
|
42
|
-
};
|
|
43
|
-
res.status(e.status || 500).send(errorResponse);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const exec = promisify(execCallback);
|
|
48
|
-
async function getAvailableMemoryMB() {
|
|
49
|
-
try {
|
|
50
|
-
if (process.platform === "darwin") {
|
|
51
|
-
const { stdout } = await exec("vm_stat");
|
|
52
|
-
const output = stdout.toString();
|
|
53
|
-
const headerLine = output.split("\n")[0] || "";
|
|
54
|
-
const pageSizeMatch = headerLine.match(/page size of\s+(\d+)\s+bytes/i);
|
|
55
|
-
const pageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : 16384;
|
|
56
|
-
const freeMatch = output.match(/Pages free:\s+(\d+)/);
|
|
57
|
-
const inactiveMatch = output.match(/Pages inactive:\s+(\d+)/);
|
|
58
|
-
const freePages = freeMatch ? parseInt(freeMatch[1], 10) : 0;
|
|
59
|
-
const inactivePages = inactiveMatch ? parseInt(inactiveMatch[1], 10) : 0;
|
|
60
|
-
return (freePages + inactivePages) * pageSize / (1024 * 1024);
|
|
61
|
-
} else {
|
|
62
|
-
const { stdout } = await exec("free -m");
|
|
63
|
-
const output = stdout.toString();
|
|
64
|
-
const lines = output.split("\n");
|
|
65
|
-
const memLine = lines.find((l) => l.startsWith("Mem:"));
|
|
66
|
-
if (memLine) {
|
|
67
|
-
const parts = memLine.split(/\s+/);
|
|
68
|
-
return parseInt(parts[6]);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
} catch (e) {
|
|
72
|
-
mayLog(`[GRIN2] Memory check failed, using fallback: ${e}`);
|
|
73
|
-
}
|
|
74
|
-
return os.freemem() / (1024 * 1024);
|
|
75
|
-
}
|
|
76
|
-
async function getMaxLesions() {
|
|
77
|
-
const availableMemoryMB = await getAvailableMemoryMB();
|
|
78
|
-
mayLog(`[GRIN2] Available system memory: ${availableMemoryMB.toFixed(0)} MB`);
|
|
79
|
-
if (availableMemoryMB < GRIN2_MEMORY_BUDGET_MB * 2) {
|
|
80
|
-
const reducedBudget = availableMemoryMB * 0.4;
|
|
81
|
-
mayLog(`[GRIN2] Reducing lesion cap due to memory constraints. New budget: ${reducedBudget.toFixed(2)} MB`);
|
|
82
|
-
const calculated = Math.floor((reducedBudget - MEMORY_BASE_MB) / MEMORY_PER_1K_LESIONS) * 1e3;
|
|
83
|
-
mayLog(`[GRIN2] Calculated lesion cap based on memory: ${calculated.toLocaleString()}`);
|
|
84
|
-
return Math.max(MIN_LESIONS, Math.min(MAX_LESIONS, calculated));
|
|
85
|
-
}
|
|
86
|
-
return MAX_LESIONS;
|
|
87
|
-
}
|
|
88
|
-
let activeGrin2Jobs = 0;
|
|
89
|
-
async function runGrin2WithLimit(g, ds, request, signal) {
|
|
90
|
-
if (activeGrin2Jobs >= GRIN2_CONCURRENCY_LIMIT) {
|
|
91
|
-
const error = new Error(
|
|
92
|
-
`GRIN2 analysis queue is full (${GRIN2_CONCURRENCY_LIMIT} concurrent analyses). Please try again in a few minutes.`
|
|
93
|
-
);
|
|
94
|
-
error.status = 429;
|
|
95
|
-
error.statusCode = 429;
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
98
|
-
activeGrin2Jobs++;
|
|
99
|
-
mayLog(`[GRIN2] Starting analysis. Active jobs: ${activeGrin2Jobs}/${GRIN2_CONCURRENCY_LIMIT}`);
|
|
100
|
-
try {
|
|
101
|
-
return await runGrin2(g, ds, request, signal);
|
|
102
|
-
} finally {
|
|
103
|
-
activeGrin2Jobs--;
|
|
104
|
-
mayLog(`[GRIN2] Analysis complete. Active jobs: ${activeGrin2Jobs}/${GRIN2_CONCURRENCY_LIMIT}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function buildLesionTypeMap(availableOptions) {
|
|
108
|
-
const lesionTypeMap = {};
|
|
109
|
-
for (const option of availableOptions) {
|
|
110
|
-
const dt = optionToDt[option];
|
|
111
|
-
if (!dt || !dt2lesion[dt]) continue;
|
|
112
|
-
dt2lesion[dt].lesionTypes.forEach((lt) => {
|
|
113
|
-
lesionTypeMap[lt.lesionType] = lt.name;
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
return lesionTypeMap;
|
|
117
|
-
}
|
|
118
|
-
function getCnvLesionType(isGain) {
|
|
119
|
-
const cnvConfig = dt2lesion[dtcnv];
|
|
120
|
-
const targetName = isGain ? "Gain" : "Loss";
|
|
121
|
-
const lesionType = cnvConfig.lesionTypes.find((lt) => lt.name === targetName);
|
|
122
|
-
if (!lesionType) {
|
|
123
|
-
throw new Error(`CNV lesion type '${targetName}' not found`);
|
|
124
|
-
}
|
|
125
|
-
return lesionType.lesionType;
|
|
126
|
-
}
|
|
127
|
-
async function runGrin2(g, ds, request, signal) {
|
|
128
|
-
const chromosomelist = {};
|
|
129
|
-
for (const c in g.majorchr) {
|
|
130
|
-
chromosomelist[c] = g.majorchr[c];
|
|
131
|
-
}
|
|
132
|
-
const {
|
|
133
|
-
result: cacheResult,
|
|
134
|
-
cacheFile,
|
|
135
|
-
freshCompute,
|
|
136
|
-
processingTime,
|
|
137
|
-
grin2AnalysisTime
|
|
138
|
-
} = await getGrin2CacheResult(request, g, ds, chromosomelist, signal);
|
|
139
|
-
const { resultData, processing } = cacheResult;
|
|
140
|
-
const rustInput = {
|
|
141
|
-
file: cacheFile,
|
|
142
|
-
type: "grin2",
|
|
143
|
-
chrSizes: chromosomelist,
|
|
144
|
-
plot_width: request.width,
|
|
145
|
-
plot_height: request.height,
|
|
146
|
-
device_pixel_ratio: request.devicePixelRatio,
|
|
147
|
-
png_dot_radius: request.pngDotRadius,
|
|
148
|
-
lesion_type_colors: request.lesionTypeColors,
|
|
149
|
-
q_value_threshold: request.qValueThreshold,
|
|
150
|
-
max_capped_points: request.maxCappedPoints,
|
|
151
|
-
hard_cap: request.hardCap,
|
|
152
|
-
bin_size: request.binSize
|
|
153
|
-
};
|
|
154
|
-
const manhattanPlotStart = Date.now();
|
|
155
|
-
const rsResult = await run_rust("manhattan_plot", JSON.stringify(rustInput), [], { signal });
|
|
156
|
-
const manhattanPlotTime = Date.now() - manhattanPlotStart;
|
|
157
|
-
mayLog(`[GRIN2] Manhattan plot generation took ${formatElapsedTime(manhattanPlotTime)}`);
|
|
158
|
-
const manhattanPlotData = JSON.parse(rsResult);
|
|
159
|
-
if (!manhattanPlotData?.png) {
|
|
160
|
-
throw new Error("Invalid Rust output: missing PNG data");
|
|
161
|
-
}
|
|
162
|
-
const totalTime = processingTime + grin2AnalysisTime + manhattanPlotTime;
|
|
163
|
-
const lesionTypeRows = [];
|
|
164
|
-
if (processing.lesionCounts?.byType) {
|
|
165
|
-
const typeLabels = {};
|
|
166
|
-
Object.values(dt2lesion).forEach((config) => {
|
|
167
|
-
config.lesionTypes.forEach((lt) => {
|
|
168
|
-
typeLabels[lt.lesionType] = lt.name;
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
for (const [type, data] of Object.entries(processing.lesionCounts.byType)) {
|
|
172
|
-
const { count, samples } = data;
|
|
173
|
-
lesionTypeRows.push([typeLabels[type] || type, `${count.toLocaleString()} (${samples} samples)`]);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
const capWarningRows = [];
|
|
177
|
-
const expectedToProcess = processing.totalSamples - processing.failedSamples;
|
|
178
|
-
if (processing.processedSamples < expectedToProcess) {
|
|
179
|
-
capWarningRows.push([
|
|
180
|
-
"Note",
|
|
181
|
-
`Lesion cap of ${processing.lesionCap?.toLocaleString()} was reached before all samples could be processed. Analysis ran on ${processing.processedSamples.toLocaleString()} of ${expectedToProcess.toLocaleString()} samples.`
|
|
182
|
-
]);
|
|
183
|
-
}
|
|
184
|
-
const response = {
|
|
185
|
-
status: "success",
|
|
186
|
-
fromCache: !freshCompute,
|
|
187
|
-
pngImg: manhattanPlotData.png,
|
|
188
|
-
plotData: manhattanPlotData.plot_data,
|
|
189
|
-
topGeneTable: resultData.topGeneTable,
|
|
190
|
-
stats: {
|
|
191
|
-
lst: [
|
|
192
|
-
{
|
|
193
|
-
name: "GRIN2 Processing Summary",
|
|
194
|
-
rows: [
|
|
195
|
-
["Total Genes", resultData.totalGenes.toLocaleString()],
|
|
196
|
-
["Showing Top", resultData.showingTop.toLocaleString()],
|
|
197
|
-
["Cache File Name", cacheFile],
|
|
198
|
-
["Total Samples", processing.totalSamples.toLocaleString()],
|
|
199
|
-
["Processed Samples", processing.processedSamples.toLocaleString()],
|
|
200
|
-
["Unprocessed Samples", (processing.unprocessedSamples ?? 0).toLocaleString()],
|
|
201
|
-
["Failed Samples", processing.failedSamples.toLocaleString()],
|
|
202
|
-
["Failed Files", (processing.failedFiles?.length ?? 0).toLocaleString()],
|
|
203
|
-
["Total Lesions", processing.totalLesions.toLocaleString()],
|
|
204
|
-
["Processed Lesions", processing.processedLesions.toLocaleString()]
|
|
205
|
-
]
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
name: "Lesion Counts",
|
|
209
|
-
rows: lesionTypeRows
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
name: "Memory Usage",
|
|
213
|
-
rows: [
|
|
214
|
-
["Start", `${resultData.memory?.start} MB`],
|
|
215
|
-
["After prep", `${resultData.memory?.after_prep} MB`],
|
|
216
|
-
["After overlaps", `${resultData.memory?.after_overlaps} MB`],
|
|
217
|
-
["After counts", `${resultData.memory?.after_counts} MB`],
|
|
218
|
-
["After stats", `${resultData.memory?.after_stats} MB`],
|
|
219
|
-
["Peak", `${resultData.memory?.peak} MB`]
|
|
220
|
-
]
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
name: "Timing",
|
|
224
|
-
rows: [
|
|
225
|
-
["Processing", freshCompute ? formatElapsedTime(processingTime) : "cached"],
|
|
226
|
-
["GRIN2", freshCompute ? formatElapsedTime(grin2AnalysisTime) : "cached"],
|
|
227
|
-
["Plotting", formatElapsedTime(manhattanPlotTime)],
|
|
228
|
-
["Total", formatElapsedTime(totalTime)],
|
|
229
|
-
...capWarningRows
|
|
230
|
-
]
|
|
231
|
-
}
|
|
232
|
-
]
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
return response;
|
|
236
|
-
}
|
|
237
|
-
function grin2KeyInputs(req) {
|
|
238
|
-
return {
|
|
239
|
-
genome: req.genome,
|
|
240
|
-
dslabel: req.dslabel,
|
|
241
|
-
filter: req.filter ?? null,
|
|
242
|
-
snvindelOptions: req.snvindelOptions ?? null,
|
|
243
|
-
cnvOptions: req.cnvOptions ?? null,
|
|
244
|
-
fusionOptions: req.fusionOptions ?? null,
|
|
245
|
-
svOptions: req.svOptions ?? null,
|
|
246
|
-
maxGenesToShow: req.maxGenesToShow ?? null
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
async function getGrin2CacheResult(request, g, ds, chromosomelist, signal) {
|
|
250
|
-
let processingTime = 0;
|
|
251
|
-
let grin2AnalysisTime = 0;
|
|
252
|
-
let freshCompute = false;
|
|
253
|
-
const {
|
|
254
|
-
result,
|
|
255
|
-
cacheId,
|
|
256
|
-
cacheFilePath: cacheFile
|
|
257
|
-
} = await cacheOrRecompute({
|
|
258
|
-
computeArgument: grin2KeyInputs(request),
|
|
259
|
-
cacheSubdir: "grin2",
|
|
260
|
-
computeFresh: async () => {
|
|
261
|
-
freshCompute = true;
|
|
262
|
-
const out = await runGrin2Fresh(request, g, ds, chromosomelist, signal);
|
|
263
|
-
processingTime = out.processingTime;
|
|
264
|
-
grin2AnalysisTime = out.grin2AnalysisTime;
|
|
265
|
-
return out.cacheResult;
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
return { result, cacheId, cacheFile, freshCompute, processingTime, grin2AnalysisTime };
|
|
269
|
-
}
|
|
270
|
-
async function runGrin2Fresh(request, g, ds, chromosomelist, signal) {
|
|
271
|
-
const startTime = Date.now();
|
|
272
|
-
const samples = await get_samples(
|
|
273
|
-
request,
|
|
274
|
-
ds,
|
|
275
|
-
true
|
|
276
|
-
// must set to true to return sample name to be able to access file. FIXME this can let names revealed to grin2 client, may need to apply access control
|
|
277
|
-
);
|
|
278
|
-
const cohortTime = Date.now() - startTime;
|
|
279
|
-
mayLog(`[GRIN2] Retrieved ${samples.length.toLocaleString()} samples in ${formatElapsedTime(cohortTime)}`);
|
|
280
|
-
if (samples.length === 0) {
|
|
281
|
-
throw new Error("No samples found matching the provided filter criteria");
|
|
282
|
-
}
|
|
283
|
-
const processingStartTime = Date.now();
|
|
284
|
-
const { lesions, processing } = await processSampleData(samples, ds, request);
|
|
285
|
-
if (!processing) throw new Error("Processing summary is missing");
|
|
286
|
-
const processingTime = Date.now() - processingStartTime;
|
|
287
|
-
mayLog(`[GRIN2] Data processing took ${formatElapsedTime(processingTime)}`);
|
|
288
|
-
mayLog(
|
|
289
|
-
`[GRIN2] Processing summary: ${processing?.processedSamples ?? 0}/${processing?.totalSamples ?? samples.length} samples processed successfully`
|
|
290
|
-
);
|
|
291
|
-
if (processing?.failedSamples !== void 0 && processing.failedSamples > 0) {
|
|
292
|
-
mayLog(`[GRIN2] Warning: ${processing.failedSamples} samples failed to process`);
|
|
293
|
-
}
|
|
294
|
-
if (lesions.length === 0) {
|
|
295
|
-
throw new Error("No lesions found after processing all samples. Check filter criteria and input data.");
|
|
296
|
-
}
|
|
297
|
-
const availableDataTypes = Object.keys(optionToDt).filter((key) => key in request);
|
|
298
|
-
const pyInput = {
|
|
299
|
-
genedb: path.join(serverconfig.tpmasterdir, g.genedb.dbfile),
|
|
300
|
-
chromosomelist,
|
|
301
|
-
lesion: JSON.stringify(lesions),
|
|
302
|
-
maxGenesToShow: request.maxGenesToShow,
|
|
303
|
-
lesionTypeMap: buildLesionTypeMap(availableDataTypes)
|
|
304
|
-
};
|
|
305
|
-
const grin2AnalysisStart = Date.now();
|
|
306
|
-
const pyResult = await run_python("grin2PpWrapper.py", JSON.stringify(pyInput), { signal });
|
|
307
|
-
if (pyResult.stderr?.trim()) {
|
|
308
|
-
mayLog(`[GRIN2] Python stderr: ${pyResult.stderr}`);
|
|
309
|
-
if (pyResult.stderr.includes("ERROR:")) {
|
|
310
|
-
throw new Error(`Python script error: ${pyResult.stderr}`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
const grin2AnalysisTime = Date.now() - grin2AnalysisStart;
|
|
314
|
-
mayLog(`[GRIN2] Python processing took ${formatElapsedTime(grin2AnalysisTime)}`);
|
|
315
|
-
const resultData = JSON.parse(pyResult);
|
|
316
|
-
const cacheResult = {
|
|
317
|
-
resultData,
|
|
318
|
-
processing
|
|
319
|
-
};
|
|
320
|
-
return { cacheResult, processingTime, grin2AnalysisTime };
|
|
321
|
-
}
|
|
322
|
-
async function processSampleData(samples, ds, request) {
|
|
323
|
-
const lesions = [];
|
|
324
|
-
const maxLesions = await getMaxLesions();
|
|
325
|
-
mayLog(`[GRIN2] Max lesions for this run: ${maxLesions.toLocaleString()}`);
|
|
326
|
-
const samplesPerType = /* @__PURE__ */ new Map();
|
|
327
|
-
const enabledTypes = [];
|
|
328
|
-
if (request.snvindelOptions) enabledTypes.push(dtsnvindel);
|
|
329
|
-
if (request.cnvOptions) enabledTypes.push(dtcnv);
|
|
330
|
-
if (request.fusionOptions) enabledTypes.push(dtfusionrna);
|
|
331
|
-
if (request.svOptions) enabledTypes.push(dtsv);
|
|
332
|
-
for (const type of enabledTypes) {
|
|
333
|
-
samplesPerType.set(type, /* @__PURE__ */ new Set());
|
|
334
|
-
}
|
|
335
|
-
const processing = {
|
|
336
|
-
totalSamples: samples.length,
|
|
337
|
-
processedSamples: 0,
|
|
338
|
-
failedSamples: 0,
|
|
339
|
-
failedFiles: [],
|
|
340
|
-
totalLesions: 0,
|
|
341
|
-
processedLesions: 0,
|
|
342
|
-
unprocessedSamples: 0
|
|
343
|
-
};
|
|
344
|
-
for (let i = 0; i < samples.length; i++) {
|
|
345
|
-
if (lesions.length >= maxLesions) {
|
|
346
|
-
const remaining = samples.length - i;
|
|
347
|
-
if (remaining > 0) processing.unprocessedSamples += remaining;
|
|
348
|
-
mayLog(`[GRIN2] Overall lesion cap (${maxLesions}) reached; stopping early.`);
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
const sample = samples[i];
|
|
352
|
-
const filepath = path.join(serverconfig.tpmasterdir, ds.queries.singleSampleMutation.folder, sample.name);
|
|
353
|
-
try {
|
|
354
|
-
await file_is_readable(filepath);
|
|
355
|
-
const mlst = JSON.parse(await read_file(filepath));
|
|
356
|
-
const { sampleLesions, contributedTypes } = processSampleMlst(sample.name, mlst, request);
|
|
357
|
-
const skipChrM = ds.queries.singleSampleMutation.discoPlot?.skipChrM;
|
|
358
|
-
const filteredLesions = skipChrM ? sampleLesions.filter((lesion) => lesion[1].toLowerCase() !== "chrm") : sampleLesions;
|
|
359
|
-
const remainingCapacity = maxLesions - lesions.length;
|
|
360
|
-
const lesionsToAdd = filteredLesions.slice(0, remainingCapacity);
|
|
361
|
-
lesions.push(...lesionsToAdd);
|
|
362
|
-
for (const type of contributedTypes) {
|
|
363
|
-
samplesPerType.get(type)?.add(sample.name);
|
|
364
|
-
}
|
|
365
|
-
processing.processedSamples += 1;
|
|
366
|
-
processing.totalLesions += filteredLesions.length;
|
|
367
|
-
} catch (error) {
|
|
368
|
-
processing.failedSamples += 1;
|
|
369
|
-
processing.failedFiles.push({
|
|
370
|
-
sampleName: sample.name,
|
|
371
|
-
filePath: filepath,
|
|
372
|
-
error: error instanceof Error ? error.message || "Unknown error" : String(error)
|
|
373
|
-
});
|
|
374
|
-
mayLog(`[GRIN2] Error processing sample ${sample.name}`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
processing.processedLesions = lesions.length;
|
|
378
|
-
processing.lesionCap = maxLesions;
|
|
379
|
-
const lesionCounts = {
|
|
380
|
-
total: lesions.length,
|
|
381
|
-
byType: {}
|
|
382
|
-
};
|
|
383
|
-
const lesionTypeCounts = {};
|
|
384
|
-
for (const lesion of lesions) {
|
|
385
|
-
const lesionType = lesion[4];
|
|
386
|
-
lesionTypeCounts[lesionType] = (lesionTypeCounts[lesionType] || 0) + 1;
|
|
387
|
-
}
|
|
388
|
-
for (const type of enabledTypes) {
|
|
389
|
-
const sampleCount = samplesPerType.get(type)?.size || 0;
|
|
390
|
-
const dtConfig = dt2lesion[type];
|
|
391
|
-
if (!dtConfig) continue;
|
|
392
|
-
dtConfig.lesionTypes.forEach((lt) => {
|
|
393
|
-
lesionCounts.byType[lt.lesionType] = {
|
|
394
|
-
count: lesionTypeCounts[lt.lesionType] || 0,
|
|
395
|
-
samples: sampleCount
|
|
396
|
-
};
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
processing.lesionCounts = lesionCounts;
|
|
400
|
-
return { lesions, processing };
|
|
401
|
-
}
|
|
402
|
-
function processSampleMlst(sampleName, mlst, request) {
|
|
403
|
-
const sampleLesions = [];
|
|
404
|
-
const contributedTypes = /* @__PURE__ */ new Set();
|
|
405
|
-
for (const m of mlst) {
|
|
406
|
-
switch (m.dt) {
|
|
407
|
-
case dtsnvindel: {
|
|
408
|
-
if (!request.snvindelOptions) break;
|
|
409
|
-
const les = filterAndConvertSnvIndel(sampleName, m, request.snvindelOptions);
|
|
410
|
-
if (les) {
|
|
411
|
-
sampleLesions.push(les);
|
|
412
|
-
contributedTypes.add(dtsnvindel);
|
|
413
|
-
}
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
case dtcnv: {
|
|
417
|
-
if (!request.cnvOptions) break;
|
|
418
|
-
const les = filterAndConvertCnv(sampleName, m, request.cnvOptions);
|
|
419
|
-
if (les) {
|
|
420
|
-
sampleLesions.push(les);
|
|
421
|
-
contributedTypes.add(dtcnv);
|
|
422
|
-
}
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
case dtfusionrna: {
|
|
426
|
-
if (!request.fusionOptions) break;
|
|
427
|
-
const les = filterAndConvertFusion(sampleName, m, request.fusionOptions);
|
|
428
|
-
if (les) {
|
|
429
|
-
if (Array.isArray(les[0])) {
|
|
430
|
-
for (const lesion of les) {
|
|
431
|
-
sampleLesions.push(lesion);
|
|
432
|
-
}
|
|
433
|
-
} else {
|
|
434
|
-
sampleLesions.push(les);
|
|
435
|
-
}
|
|
436
|
-
contributedTypes.add(dtfusionrna);
|
|
437
|
-
}
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
case dtsv: {
|
|
441
|
-
if (!request.svOptions) break;
|
|
442
|
-
const les = filterAndConvertSV(sampleName, m, request.svOptions);
|
|
443
|
-
if (les) {
|
|
444
|
-
if (Array.isArray(les[0])) {
|
|
445
|
-
for (const lesion of les) {
|
|
446
|
-
sampleLesions.push(lesion);
|
|
447
|
-
}
|
|
448
|
-
} else {
|
|
449
|
-
sampleLesions.push(les);
|
|
450
|
-
}
|
|
451
|
-
contributedTypes.add(dtsv);
|
|
452
|
-
}
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
default:
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return { sampleLesions, contributedTypes };
|
|
460
|
-
}
|
|
461
|
-
function filterAndConvertSnvIndel(sampleName, entry, options) {
|
|
462
|
-
if (!options?.consequences) {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
if (options.consequences.length > 0 && entry.class && !options.consequences.includes(entry.class)) {
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
if (!Number.isInteger(entry.pos)) {
|
|
469
|
-
return null;
|
|
470
|
-
}
|
|
471
|
-
if (options.mafFilter?.lst?.length) {
|
|
472
|
-
if (!Array.isArray(entry.vafs)) return null;
|
|
473
|
-
const copy = { dt: dtsnvindel };
|
|
474
|
-
for (const v of entry.vafs) {
|
|
475
|
-
copy[v.id] = v.refCount + "," + v.altCount;
|
|
476
|
-
}
|
|
477
|
-
try {
|
|
478
|
-
if (!mayFilterByMaf(options.mafFilter, copy)) return null;
|
|
479
|
-
} catch (e) {
|
|
480
|
-
mayLog("mayFilterByMaf() crashed on a snvindel " + (e instanceof Error ? e.message : String(e)));
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
const start = entry.pos;
|
|
485
|
-
const end = entry.pos;
|
|
486
|
-
return [sampleName, entry.chr, start, end, dt2lesion[dtsnvindel].lesionTypes[0].lesionType];
|
|
487
|
-
}
|
|
488
|
-
function filterAndConvertCnv(sampleName, entry, options) {
|
|
489
|
-
if (!options || options.gainThreshold === void 0 || options.lossThreshold === void 0 || options.maxSegLength === void 0) {
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
if (!Number.isInteger(entry.start)) {
|
|
493
|
-
return null;
|
|
494
|
-
}
|
|
495
|
-
if (!Number.isInteger(entry.stop)) {
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
if (options.maxSegLength > 0 && entry.stop - entry.start > options.maxSegLength) {
|
|
499
|
-
return null;
|
|
500
|
-
}
|
|
501
|
-
const isGain = entry.value >= options.gainThreshold;
|
|
502
|
-
const isLoss = entry.value <= options.lossThreshold;
|
|
503
|
-
if (!isGain && !isLoss) return null;
|
|
504
|
-
const lesionType = getCnvLesionType(isGain);
|
|
505
|
-
const start = entry.start;
|
|
506
|
-
const end = entry.stop;
|
|
507
|
-
return [sampleName, entry.chr, start, end, lesionType];
|
|
508
|
-
}
|
|
509
|
-
function filterAndConvertFusion(sampleName, entry, _options) {
|
|
510
|
-
if (!entry.chrA || entry.posA === void 0) {
|
|
511
|
-
return null;
|
|
512
|
-
}
|
|
513
|
-
const startA = entry.posA;
|
|
514
|
-
const endA = entry.posA;
|
|
515
|
-
const lesionA = [sampleName, entry.chrA, startA, endA, dt2lesion[dtfusionrna].lesionTypes[0].lesionType];
|
|
516
|
-
if (entry.chrB && entry.posB !== void 0) {
|
|
517
|
-
const startB = entry.posB;
|
|
518
|
-
const endB = entry.posB;
|
|
519
|
-
const lesionB = [sampleName, entry.chrB, startB, endB, dt2lesion[dtfusionrna].lesionTypes[0].lesionType];
|
|
520
|
-
return [lesionA, lesionB];
|
|
521
|
-
}
|
|
522
|
-
return lesionA;
|
|
523
|
-
}
|
|
524
|
-
function filterAndConvertSV(sampleName, entry, _options) {
|
|
525
|
-
if (!entry.chrA || entry.posA === void 0) {
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
const startA = entry.posA;
|
|
529
|
-
const endA = entry.posA;
|
|
530
|
-
const lesionA = [sampleName, entry.chrA, startA, endA, dt2lesion[dtsv].lesionTypes[0].lesionType];
|
|
531
|
-
if (entry.chrB && entry.posB !== void 0) {
|
|
532
|
-
const startB = entry.posB;
|
|
533
|
-
const endB = entry.posB;
|
|
534
|
-
const lesionB = [sampleName, entry.chrB, startB, endB, dt2lesion[dtsv].lesionTypes[0].lesionType];
|
|
535
|
-
return [lesionA, lesionB];
|
|
536
|
-
}
|
|
537
|
-
return lesionA;
|
|
538
|
-
}
|
|
539
|
-
export {
|
|
540
|
-
init
|
|
541
|
-
};
|