@sjcrh/proteinpaint-server 2.136.0 → 2.137.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 +2 -2
- package/routes/grin2.js +245 -0
- package/src/app.js +493 -230
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.137.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "a genomics visualization tool for exploring a cohort's genotype and phenotype data",
|
|
6
6
|
"main": "src/app.js",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@sjcrh/proteinpaint-r": "2.130.0",
|
|
65
65
|
"@sjcrh/proteinpaint-rust": "2.136.0",
|
|
66
66
|
"@sjcrh/proteinpaint-shared": "2.136.0",
|
|
67
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
67
|
+
"@sjcrh/proteinpaint-types": "2.137.0",
|
|
68
68
|
"@types/express": "^5.0.0",
|
|
69
69
|
"@types/express-session": "^1.18.1",
|
|
70
70
|
"better-sqlite3": "^9.4.1",
|
package/routes/grin2.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { GRIN2Payload } from "#types/checkers";
|
|
2
|
+
import serverconfig from "#src/serverconfig.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { run_python } from "@sjcrh/proteinpaint-python";
|
|
5
|
+
import { mayLog } from "#src/helpers.ts";
|
|
6
|
+
import { get_samples } from "#src/termdb.sql.js";
|
|
7
|
+
import { read_file, file_is_readable } from "#src/utils.js";
|
|
8
|
+
import { dtsnvindel, dtcnv, dtfusionrna } from "#shared/common.js";
|
|
9
|
+
const api = {
|
|
10
|
+
endpoint: "grin2",
|
|
11
|
+
methods: {
|
|
12
|
+
get: {
|
|
13
|
+
...GRIN2Payload,
|
|
14
|
+
init
|
|
15
|
+
},
|
|
16
|
+
post: {
|
|
17
|
+
...GRIN2Payload,
|
|
18
|
+
init
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function init({ genomes }) {
|
|
23
|
+
return async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const request = req.query;
|
|
26
|
+
const g = genomes[request.genome];
|
|
27
|
+
if (!g)
|
|
28
|
+
throw new Error("genome missing");
|
|
29
|
+
const ds = g.datasets?.[request.dslabel];
|
|
30
|
+
if (!ds)
|
|
31
|
+
throw new Error("ds missing");
|
|
32
|
+
if (!ds.queries?.singleSampleMutation)
|
|
33
|
+
throw new Error("singleSampleMutation query missing from dataset");
|
|
34
|
+
const result = await runGrin2(g, ds, request);
|
|
35
|
+
res.json(result);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error("[GRIN2] Error stack:", e.stack);
|
|
38
|
+
const errorResponse = {
|
|
39
|
+
status: "error",
|
|
40
|
+
error: e.message || String(e)
|
|
41
|
+
};
|
|
42
|
+
res.status(500).send(errorResponse);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function runGrin2(g, ds, request) {
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
mayLog("[GRIN2] Getting samples from cohort filter...");
|
|
49
|
+
const samples = await get_samples(
|
|
50
|
+
request.filter,
|
|
51
|
+
ds,
|
|
52
|
+
true
|
|
53
|
+
// 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
|
|
54
|
+
);
|
|
55
|
+
const cohortTime = Date.now() - startTime;
|
|
56
|
+
mayLog(`[GRIN2] Retrieved ${samples.length.toLocaleString()} samples in ${Math.round(cohortTime / 1e3)} seconds`);
|
|
57
|
+
if (samples.length === 0) {
|
|
58
|
+
throw new Error("No samples found matching the provided filter criteria");
|
|
59
|
+
}
|
|
60
|
+
mayLog("[GRIN2] Processing sample data...");
|
|
61
|
+
const processingStartTime = Date.now();
|
|
62
|
+
const { lesionData, processingSummary } = await processSampleData(samples, ds, request);
|
|
63
|
+
const processingTime = Date.now() - processingStartTime;
|
|
64
|
+
const processingTimeToPrint = Math.round(processingTime / 1e3);
|
|
65
|
+
mayLog(`[GRIN2] Data processing took ${processingTimeToPrint} seconds`);
|
|
66
|
+
mayLog(
|
|
67
|
+
`[GRIN2] Processing summary: ${processingSummary?.successfulSamples ?? 0}/${processingSummary?.totalSamples ?? samples.length} samples processed successfully`
|
|
68
|
+
);
|
|
69
|
+
if (processingSummary && processingSummary.failedSamples > 0) {
|
|
70
|
+
mayLog(`[GRIN2] Warning: ${processingSummary.failedSamples} samples failed to process`);
|
|
71
|
+
}
|
|
72
|
+
if (lesionData.length === 0) {
|
|
73
|
+
throw new Error("No lesions found after processing all samples. Check filter criteria and input data.");
|
|
74
|
+
}
|
|
75
|
+
const pyInput = {
|
|
76
|
+
genedb: path.join(serverconfig.tpmasterdir, g.genedb.dbfile),
|
|
77
|
+
chromosomelist: {},
|
|
78
|
+
lesion: JSON.stringify(lesionData)
|
|
79
|
+
};
|
|
80
|
+
for (const c in g.majorchr) {
|
|
81
|
+
if (ds.queries.singleSampleMutation.discoPlot?.skipChrM) {
|
|
82
|
+
if (c.toLowerCase() == "chrm")
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
pyInput.chromosomelist[c] = g.majorchr[c];
|
|
86
|
+
}
|
|
87
|
+
mayLog(`[GRIN2] Prepared ${lesionData.length.toLocaleString()} lesions for analysis`);
|
|
88
|
+
const grin2AnalysisStart = Date.now();
|
|
89
|
+
const pyResult = await run_python("grin2PpWrapper.py", JSON.stringify(pyInput));
|
|
90
|
+
if (pyResult.stderr?.trim()) {
|
|
91
|
+
mayLog(`[GRIN2] Python stderr: ${pyResult.stderr}`);
|
|
92
|
+
if (pyResult.stderr.includes("ERROR:")) {
|
|
93
|
+
throw new Error(`Python script error: ${pyResult.stderr}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const grin2AnalysisTime = Date.now() - grin2AnalysisStart;
|
|
97
|
+
const grin2AnalysisTimeToPrint = Math.round(grin2AnalysisTime / 1e3);
|
|
98
|
+
mayLog(`[GRIN2] Python processing took ${grin2AnalysisTimeToPrint} seconds`);
|
|
99
|
+
const resultData = JSON.parse(pyResult.stdout);
|
|
100
|
+
if (!resultData?.png?.[0]) {
|
|
101
|
+
throw new Error("Invalid Python output: missing PNG data");
|
|
102
|
+
}
|
|
103
|
+
const totalTime = Math.round((Date.now() - startTime) / 1e3);
|
|
104
|
+
const response = {
|
|
105
|
+
status: "success",
|
|
106
|
+
pngImg: resultData.png[0],
|
|
107
|
+
topGeneTable: resultData.topGeneTable,
|
|
108
|
+
totalGenes: resultData.totalGenes,
|
|
109
|
+
showingTop: resultData.showingTop,
|
|
110
|
+
timing: {
|
|
111
|
+
processingTime: processingTimeToPrint,
|
|
112
|
+
grin2Time: grin2AnalysisTimeToPrint,
|
|
113
|
+
totalTime
|
|
114
|
+
},
|
|
115
|
+
processingSummary
|
|
116
|
+
};
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
async function processSampleData(samples, ds, request) {
|
|
120
|
+
const lesions = [];
|
|
121
|
+
let lesionId = 1;
|
|
122
|
+
const processingSummary = {
|
|
123
|
+
totalSamples: samples.length,
|
|
124
|
+
successfulSamples: 0,
|
|
125
|
+
failedSamples: 0,
|
|
126
|
+
failedFiles: []
|
|
127
|
+
};
|
|
128
|
+
mayLog(`[GRIN2] Processing JSON files for ${samples.length.toLocaleString()} samples`);
|
|
129
|
+
for (const sample of samples) {
|
|
130
|
+
try {
|
|
131
|
+
const filepath = path.join(serverconfig.tpmasterdir, ds.queries.singleSampleMutation.folder, sample.name);
|
|
132
|
+
await file_is_readable(filepath);
|
|
133
|
+
const mlst = JSON.parse(await read_file(filepath));
|
|
134
|
+
const sampleLesions = await processSampleMlst(sample.name, mlst, lesionId, request);
|
|
135
|
+
lesions.push(...sampleLesions);
|
|
136
|
+
lesionId += sampleLesions.length;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
mayLog(
|
|
139
|
+
`[GRIN2] Error processing sample ${sample.name}: ${typeof error === "object" && error !== null && "message" in error ? error.message : String(error)}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
mayLog(`[GRIN2] Total lesions processed: ${lesions.length.toLocaleString()}`);
|
|
144
|
+
return {
|
|
145
|
+
lesionData: lesions,
|
|
146
|
+
processingSummary
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function processSampleMlst(sampleName, mlst, startId, request) {
|
|
150
|
+
const lesions = [];
|
|
151
|
+
for (const m of mlst) {
|
|
152
|
+
switch (m.dt) {
|
|
153
|
+
case dtsnvindel: {
|
|
154
|
+
if (!request.snvindelOptions)
|
|
155
|
+
break;
|
|
156
|
+
const snvIndelLesion = filterAndConvertSnvIndel(sampleName, m, request.snvindelOptions);
|
|
157
|
+
if (snvIndelLesion)
|
|
158
|
+
lesions.push(snvIndelLesion);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case dtcnv: {
|
|
162
|
+
if (!request.cnvOptions)
|
|
163
|
+
break;
|
|
164
|
+
const cnvLesion = filterAndConvertCnv(sampleName, m, request.cnvOptions);
|
|
165
|
+
if (cnvLesion)
|
|
166
|
+
lesions.push(cnvLesion);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case dtfusionrna: {
|
|
170
|
+
if (!request.fusionOptions)
|
|
171
|
+
break;
|
|
172
|
+
const fusionLesion = filterAndConvertFusion(sampleName, m, request.fusionOptions);
|
|
173
|
+
if (fusionLesion)
|
|
174
|
+
lesions.push(fusionLesion);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
mayLog(`[GRIN2] Unknown data type "${m.dt}" in sample ${sampleName}, skipping entry`);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return lesions;
|
|
183
|
+
}
|
|
184
|
+
function filterAndConvertSnvIndel(sampleName, entry, options) {
|
|
185
|
+
const opts = {
|
|
186
|
+
minTotalDepth: options?.minTotalDepth ?? 10,
|
|
187
|
+
minAltAlleleCount: options?.minAltAlleleCount ?? 2,
|
|
188
|
+
consequences: options?.consequences ?? [],
|
|
189
|
+
hyperMutator: options?.hyperMutator ?? 1e3
|
|
190
|
+
};
|
|
191
|
+
if (opts.consequences.length > 0 && entry.consequence && !opts.consequences.includes(entry.consequence)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return [
|
|
195
|
+
sampleName,
|
|
196
|
+
normalizeChromosome(entry.chromosome || entry.chr),
|
|
197
|
+
String(entry.start || entry.position),
|
|
198
|
+
String(entry.end || entry.position),
|
|
199
|
+
"mutation"
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
function filterAndConvertCnv(sampleName, entry, options) {
|
|
203
|
+
const opts = {
|
|
204
|
+
lossThreshold: options?.lossThreshold ?? -0.4,
|
|
205
|
+
gainThreshold: options?.gainThreshold ?? 0.3,
|
|
206
|
+
maxSegLength: options?.maxSegLength ?? 0,
|
|
207
|
+
minSegLength: options?.minSegLength ?? 0,
|
|
208
|
+
hyperMutator: options?.hyperMutator ?? 500
|
|
209
|
+
};
|
|
210
|
+
const isGain = entry.log2Ratio >= opts.gainThreshold;
|
|
211
|
+
const isLoss = entry.log2Ratio <= opts.lossThreshold;
|
|
212
|
+
if (!isGain && !isLoss)
|
|
213
|
+
return null;
|
|
214
|
+
const lesionType = entry.log2Ratio >= opts.gainThreshold ? "gain" : "loss";
|
|
215
|
+
return [
|
|
216
|
+
sampleName,
|
|
217
|
+
normalizeChromosome(entry.chromosome || entry.chr),
|
|
218
|
+
String(entry.start || entry.begin),
|
|
219
|
+
String(entry.end || entry.stop),
|
|
220
|
+
lesionType
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
function filterAndConvertFusion(sampleName, entry, options) {
|
|
224
|
+
const opts = {
|
|
225
|
+
fusionTypes: options?.fusionTypes ?? ["gene-gene", "gene-intergenic", "readthrough"],
|
|
226
|
+
minConfidence: options?.minConfidence ?? 0.7
|
|
227
|
+
};
|
|
228
|
+
if (entry.fusionType && !opts.fusionTypes.includes(entry.fusionType))
|
|
229
|
+
return null;
|
|
230
|
+
if (entry.confidence && entry.confidence < opts.minConfidence)
|
|
231
|
+
return null;
|
|
232
|
+
return [
|
|
233
|
+
sampleName,
|
|
234
|
+
normalizeChromosome(entry.chromosome || entry.chr),
|
|
235
|
+
String(entry.start || entry.position),
|
|
236
|
+
String(entry.end || entry.position),
|
|
237
|
+
"fusion"
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
function normalizeChromosome(chrom) {
|
|
241
|
+
return chrom.startsWith("chr") ? chrom : `chr${chrom}`;
|
|
242
|
+
}
|
|
243
|
+
export {
|
|
244
|
+
api
|
|
245
|
+
};
|