@sjcrh/proteinpaint-server 2.189.0 → 2.190.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 +9 -12
- package/routes/aiProjectAdmin.js +2 -28
- package/routes/aiProjectSelectedWSImages.js +2 -16
- package/routes/brainImaging.js +1 -15
- package/routes/brainImagingSamples.js +1 -15
- package/routes/burden.js +1 -15
- package/routes/correlationVolcano.js +1 -15
- package/routes/dataset.js +1 -16
- package/routes/deleteWSITileSelection.js +2 -12
- package/routes/dsdata.js +1 -19
- package/routes/gdc.grin2.list.js +1 -15
- package/routes/gdc.grin2.run.js +1 -15
- package/routes/gdc.maf.js +1 -15
- package/routes/gdc.mafBuild.js +1 -15
- package/routes/genesetEnrichment.js +129 -97
- package/routes/grin2.js +110 -79
- package/routes/saveWSIAnnotation.js +2 -13
- package/routes/termdb.DE.js +137 -54
- package/routes/termdb.categories.js +2 -16
- package/routes/termdb.chat.js +169 -1076
- package/routes/termdb.cluster.js +5 -16
- package/routes/termdb.config.js +12 -17
- package/routes/termdb.descrstats.js +2 -16
- package/routes/termdb.diffMeth.js +100 -21
- package/routes/termdb.geneRanking.js +139 -0
- package/routes/termdb.proteome.js +1 -15
- package/routes/termdb.runChart.js +16 -30
- package/routes/termdb.sampleScatter.js +7 -97
- package/routes/termdb.singleCellPlots.js +159 -0
- package/routes/termdb.singlecellSamples.js +6 -16
- package/routes/termdb.violinBox.js +1 -15
- package/routes/wsimages.js +1 -16
- package/src/app.js +3984 -4103
- package/routes/_template_.js +0 -33
- package/routes/aiProjectTrainModel.js +0 -68
- package/routes/alphaGenome.js +0 -41
- package/routes/alphaGenomeTypes.js +0 -36
- package/routes/dzimages.js +0 -55
- package/routes/gene2canonicalisoform.js +0 -37
- package/routes/genelookup.js +0 -32
- package/routes/genesetOverrepresentation.js +0 -49
- package/routes/genomes.js +0 -150
- package/routes/healthcheck.js +0 -35
- package/routes/hicdata.js +0 -74
- package/routes/hicgenome.js +0 -75
- package/routes/hicstat.js +0 -35
- package/routes/img.js +0 -46
- package/routes/isoformlst.js +0 -48
- package/routes/ntseq.js +0 -36
- package/routes/pdomain.js +0 -53
- package/routes/profile.barchart2.js +0 -114
- package/routes/profile.forms2.js +0 -107
- package/routes/profile.polar2.js +0 -101
- package/routes/profile.radar2.js +0 -112
- package/routes/profile.radarFacility2.js +0 -148
- package/routes/sampledzimages.js +0 -48
- package/routes/samplewsimages.js +0 -60
- package/routes/snp.js +0 -98
- package/routes/termdb.chat2.js +0 -217
- package/routes/termdb.chat3.js +0 -209
- package/routes/termdb.cohort.summary.js +0 -37
- package/routes/termdb.cohorts.js +0 -41
- package/routes/termdb.dapVolcano.js +0 -80
- package/routes/termdb.dmr.js +0 -93
- package/routes/termdb.filterTermValues.js +0 -89
- package/routes/termdb.isoformAvailability.js +0 -35
- package/routes/termdb.numericcategories.js +0 -46
- package/routes/termdb.percentile.js +0 -66
- package/routes/termdb.profileFormScores.js +0 -92
- package/routes/termdb.profileScores.js +0 -113
- package/routes/termdb.rootterm.js +0 -39
- package/routes/termdb.sampleImages.js +0 -63
- package/routes/termdb.singleSampleMutation.js +0 -75
- package/routes/termdb.singlecellDEgenes.js +0 -55
- package/routes/termdb.singlecellData.js +0 -39
- package/routes/termdb.termchildren.js +0 -42
- package/routes/termdb.termsbyids.js +0 -50
- package/routes/termdb.topMutatedGenes.js +0 -127
- package/routes/termdb.topTermsByType.js +0 -96
- package/routes/termdb.topVariablyExpressedGenes.js +0 -132
- package/routes/tileserver.js +0 -68
- package/routes/wsisamples.js +0 -71
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { genesetEnrichmentPayload } from "#types/checkers";
|
|
2
|
-
import crypto from "crypto";
|
|
3
1
|
import fs from "fs";
|
|
4
2
|
import path from "path";
|
|
5
3
|
import serverconfig from "#src/serverconfig.js";
|
|
@@ -7,21 +5,10 @@ import { run_python } from "@sjcrh/proteinpaint-python";
|
|
|
7
5
|
import { run_rust } from "@sjcrh/proteinpaint-rust";
|
|
8
6
|
import { mayLog } from "#src/helpers.ts";
|
|
9
7
|
import { formatElapsedTime } from "#shared";
|
|
10
|
-
import {
|
|
8
|
+
import { getDeCacheResult } from "../routes/termdb.DE.ts";
|
|
9
|
+
import { getDmCacheResult } from "../routes/termdb.diffMeth.ts";
|
|
10
|
+
import { cacheOrRecompute } from "#src/utils/cacheOrRecompute.ts";
|
|
11
11
|
import { get_ds_tdb } from "#src/termdb.js";
|
|
12
|
-
const api = {
|
|
13
|
-
endpoint: "genesetEnrichment",
|
|
14
|
-
methods: {
|
|
15
|
-
get: {
|
|
16
|
-
...genesetEnrichmentPayload,
|
|
17
|
-
init
|
|
18
|
-
},
|
|
19
|
-
post: {
|
|
20
|
-
...genesetEnrichmentPayload,
|
|
21
|
-
init
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
12
|
function init({ genomes }) {
|
|
26
13
|
return async (req, res) => {
|
|
27
14
|
try {
|
|
@@ -45,7 +32,7 @@ function init({ genomes }) {
|
|
|
45
32
|
}
|
|
46
33
|
});
|
|
47
34
|
} catch (e) {
|
|
48
|
-
res.send({ status: "error", error: e.message || e });
|
|
35
|
+
res.status(e.status || 500).send({ status: "error", error: e.message || e, code: e.code });
|
|
49
36
|
if (e.stack) console.log(e.stack);
|
|
50
37
|
}
|
|
51
38
|
};
|
|
@@ -57,7 +44,8 @@ async function run_genesetEnrichment_analysis(q, genomes) {
|
|
|
57
44
|
return { data: { genes, fold_change } };
|
|
58
45
|
}
|
|
59
46
|
if (q.method == "blitzgsea") {
|
|
60
|
-
return await
|
|
47
|
+
if (q.geneset_name) return await computeGseaImage({ q, genomes });
|
|
48
|
+
return await computeGseaInitial({ q, genomes });
|
|
61
49
|
}
|
|
62
50
|
if (q.method == "cerno") {
|
|
63
51
|
const { genes, fold_change } = await resolveGseaGenesAndFoldChange({ q, genomes });
|
|
@@ -76,30 +64,137 @@ async function run_genesetEnrichment_analysis(q, genomes) {
|
|
|
76
64
|
}
|
|
77
65
|
throw new Error("Unknown method:" + q.method);
|
|
78
66
|
}
|
|
67
|
+
function gseaKeyInputs(q, genes, fold_change) {
|
|
68
|
+
return {
|
|
69
|
+
genes,
|
|
70
|
+
fold_change,
|
|
71
|
+
geneSetGroup: q.geneSetGroup,
|
|
72
|
+
num_permutations: q.num_permutations,
|
|
73
|
+
filter_non_coding_genes: q.filter_non_coding_genes
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function getGseaCacheResult({
|
|
77
|
+
q,
|
|
78
|
+
genomes
|
|
79
|
+
}) {
|
|
80
|
+
const { genes, fold_change } = await resolveGseaGenesAndFoldChange({ q, genomes });
|
|
81
|
+
const cacheArg = gseaKeyInputs(q, genes, fold_change);
|
|
82
|
+
const { result, cacheId } = await cacheOrRecompute({
|
|
83
|
+
computeArgument: cacheArg,
|
|
84
|
+
cacheSubdir: "gsea",
|
|
85
|
+
computeFresh: async () => {
|
|
86
|
+
const { table, pickleB64 } = await runGseaPythonForTable({ q, genomes, cacheArg });
|
|
87
|
+
const cacheResult = { table, pickleB64 };
|
|
88
|
+
return cacheResult;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return { result, cacheId };
|
|
92
|
+
}
|
|
93
|
+
async function computeGseaInitial({
|
|
94
|
+
q,
|
|
95
|
+
genomes
|
|
96
|
+
}) {
|
|
97
|
+
const { result } = await getGseaCacheResult({ q, genomes });
|
|
98
|
+
return result.table;
|
|
99
|
+
}
|
|
100
|
+
const pendingImageRequests = /* @__PURE__ */ new Map();
|
|
101
|
+
async function computeGseaImage({
|
|
102
|
+
q,
|
|
103
|
+
genomes
|
|
104
|
+
}) {
|
|
105
|
+
const { result, cacheId } = await getGseaCacheResult({ q, genomes });
|
|
106
|
+
const dedupKey = `${cacheId}:${q.geneset_name}`;
|
|
107
|
+
const inFlight = pendingImageRequests.get(dedupKey);
|
|
108
|
+
if (inFlight) return inFlight;
|
|
109
|
+
const work = runGseaPythonForImage({
|
|
110
|
+
q,
|
|
111
|
+
genomes,
|
|
112
|
+
pickleB64: result.pickleB64
|
|
113
|
+
}).finally(() => pendingImageRequests.delete(dedupKey));
|
|
114
|
+
pendingImageRequests.set(dedupKey, work);
|
|
115
|
+
return work;
|
|
116
|
+
}
|
|
117
|
+
async function runGseaPythonForTable({
|
|
118
|
+
q,
|
|
119
|
+
genomes,
|
|
120
|
+
cacheArg
|
|
121
|
+
}) {
|
|
122
|
+
const pyInput = buildPyInput({ ...q, geneset_name: void 0 }, genomes, cacheArg);
|
|
123
|
+
const time1 = (/* @__PURE__ */ new Date()).valueOf();
|
|
124
|
+
const gsea_output = await run_python("gsea.py", "/" + JSON.stringify(pyInput));
|
|
125
|
+
mayLog("Time taken to run blitzgsea:", formatElapsedTime(Date.now() - time1));
|
|
126
|
+
for (const line of gsea_output.split("\n")) {
|
|
127
|
+
if (line.startsWith("result: ")) {
|
|
128
|
+
const parsed = JSON.parse(line.replace("result: ", ""));
|
|
129
|
+
if (parsed?.error) throw new Error(parsed.error);
|
|
130
|
+
const { pickle_b64, ...table } = parsed;
|
|
131
|
+
if (!pickle_b64) throw new Error("gsea.py result missing pickle_b64");
|
|
132
|
+
return { table, pickleB64: pickle_b64 };
|
|
133
|
+
}
|
|
134
|
+
mayLog(line);
|
|
135
|
+
}
|
|
136
|
+
throw new Error("gsea.py did not emit a result line on the initial path");
|
|
137
|
+
}
|
|
138
|
+
async function runGseaPythonForImage({
|
|
139
|
+
q,
|
|
140
|
+
genomes,
|
|
141
|
+
pickleB64
|
|
142
|
+
}) {
|
|
143
|
+
const { genes, fold_change } = await resolveGseaGenesAndFoldChange({ q, genomes });
|
|
144
|
+
const cacheArg = gseaKeyInputs(q, genes, fold_change);
|
|
145
|
+
const pyInput = buildPyInput(q, genomes, cacheArg, pickleB64);
|
|
146
|
+
const time1 = (/* @__PURE__ */ new Date()).valueOf();
|
|
147
|
+
const gsea_output = await run_python("gsea.py", "/" + JSON.stringify(pyInput));
|
|
148
|
+
mayLog("Time taken to render gsea image:", formatElapsedTime(Date.now() - time1));
|
|
149
|
+
for (const line of gsea_output.split("\n")) {
|
|
150
|
+
if (line.startsWith("image: ")) {
|
|
151
|
+
const parsed = JSON.parse(line.replace("image: ", ""));
|
|
152
|
+
return path.join(serverconfig.cachedir, "gsea", parsed.image_file);
|
|
153
|
+
}
|
|
154
|
+
if (line.startsWith("result: ")) {
|
|
155
|
+
const parsed = JSON.parse(line.replace("result: ", ""));
|
|
156
|
+
if (parsed?.error) return { error: parsed.error };
|
|
157
|
+
}
|
|
158
|
+
mayLog(line);
|
|
159
|
+
}
|
|
160
|
+
throw new Error("gsea.py did not emit an image line on the detail path");
|
|
161
|
+
}
|
|
162
|
+
function buildPyInput(q, genomes, cacheArg, pickleB64) {
|
|
163
|
+
return {
|
|
164
|
+
genes: cacheArg.genes,
|
|
165
|
+
fold_change: cacheArg.fold_change,
|
|
166
|
+
db: genomes[q.genome].termdbs.msigdb.cohort.db.connection.name,
|
|
167
|
+
geneset_group: cacheArg.geneSetGroup,
|
|
168
|
+
genedb: path.join(serverconfig.tpmasterdir, genomes[q.genome].genedb.dbfile),
|
|
169
|
+
filter_non_coding_genes: cacheArg.filter_non_coding_genes,
|
|
170
|
+
cachedir: path.join(serverconfig.cachedir, "gsea"),
|
|
171
|
+
geneset_name: q.geneset_name,
|
|
172
|
+
num_permutations: cacheArg.num_permutations,
|
|
173
|
+
...pickleB64 ? { pickle_b64: pickleB64 } : {}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
79
176
|
async function resolveGseaGenesAndFoldChange({
|
|
80
177
|
q,
|
|
81
178
|
genomes
|
|
82
179
|
}) {
|
|
83
180
|
if (q.cacheId) {
|
|
84
181
|
if (!q.daRequest) throw new Error("daCacheMissing");
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (result.kind === "DE") {
|
|
182
|
+
const kind = q.daRequest.kind;
|
|
183
|
+
if (kind !== "DE" && kind !== "DM") throw new Error('daRequest.kind must be "DE" or "DM"');
|
|
184
|
+
if (kind === "DE") {
|
|
185
|
+
const { result: result2, cacheId: cacheId2 } = await getDeCacheResult(q.daRequest, genomes);
|
|
186
|
+
if (cacheId2 !== q.cacheId) throw new Error("cacheId does not match daRequest");
|
|
91
187
|
return {
|
|
92
|
-
genes:
|
|
93
|
-
fold_change:
|
|
188
|
+
genes: result2.geneRows.map((g) => g.gene_name),
|
|
189
|
+
fold_change: result2.geneRows.map((g) => g.fold_change)
|
|
94
190
|
};
|
|
95
191
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
throw new Error(`unexpected result kind: ${result.kind}`);
|
|
192
|
+
const { result, cacheId } = await getDmCacheResult(q.daRequest, genomes);
|
|
193
|
+
if (cacheId !== q.cacheId) throw new Error("cacheId does not match daRequest");
|
|
194
|
+
return {
|
|
195
|
+
genes: result.promoterRows.map((p) => p.gene_name),
|
|
196
|
+
fold_change: result.promoterRows.map((p) => p.fold_change)
|
|
197
|
+
};
|
|
103
198
|
}
|
|
104
199
|
if (q.dapParams) {
|
|
105
200
|
const genome = genomes[q.genome];
|
|
@@ -126,69 +221,6 @@ async function resolveGseaGenesAndFoldChange({
|
|
|
126
221
|
if (!q.genes || !q.fold_change) throw new Error("requires genes and fold_change when cacheId is absent");
|
|
127
222
|
return { genes: q.genes, fold_change: q.fold_change };
|
|
128
223
|
}
|
|
129
|
-
const pendingTracker_gsea = /* @__PURE__ */ new Map();
|
|
130
|
-
const pendingTracker_img = /* @__PURE__ */ new Map();
|
|
131
|
-
async function computeGSEA({
|
|
132
|
-
q,
|
|
133
|
-
genomes
|
|
134
|
-
}) {
|
|
135
|
-
const { genes, fold_change } = await resolveGseaGenesAndFoldChange({ q, genomes });
|
|
136
|
-
const gseaComputeArg = {
|
|
137
|
-
genes,
|
|
138
|
-
fold_change,
|
|
139
|
-
geneSetGroup: q.geneSetGroup,
|
|
140
|
-
num_permutations: q.num_permutations,
|
|
141
|
-
filter_non_coding_genes: q.filter_non_coding_genes
|
|
142
|
-
};
|
|
143
|
-
const key = stableStringify(gseaComputeArg);
|
|
144
|
-
const pickle_file = crypto.createHash("sha256").update(key).digest("hex").slice(0, 32) + ".pkl";
|
|
145
|
-
const tracker = q.geneset_name ? pendingTracker_img : pendingTracker_gsea;
|
|
146
|
-
const inFlight = tracker.get(pickle_file);
|
|
147
|
-
if (inFlight) return inFlight;
|
|
148
|
-
const work = runGseaPython({ q, genomes, gseaComputeArg, pickle_file });
|
|
149
|
-
tracker.set(pickle_file, work);
|
|
150
|
-
return work.finally(() => tracker.delete(pickle_file));
|
|
151
|
-
}
|
|
152
|
-
async function runGseaPython({
|
|
153
|
-
q,
|
|
154
|
-
genomes,
|
|
155
|
-
gseaComputeArg,
|
|
156
|
-
pickle_file
|
|
157
|
-
}) {
|
|
158
|
-
const cachedir_gsea = path.join(serverconfig.cachedir, "gsea");
|
|
159
|
-
const genesetenrichment_input = {
|
|
160
|
-
genes: gseaComputeArg.genes,
|
|
161
|
-
fold_change: gseaComputeArg.fold_change,
|
|
162
|
-
db: genomes[q.genome].termdbs.msigdb.cohort.db.connection.name,
|
|
163
|
-
geneset_group: gseaComputeArg.geneSetGroup,
|
|
164
|
-
genedb: path.join(serverconfig.tpmasterdir, genomes[q.genome].genedb.dbfile),
|
|
165
|
-
filter_non_coding_genes: gseaComputeArg.filter_non_coding_genes,
|
|
166
|
-
cachedir: cachedir_gsea,
|
|
167
|
-
pickle_file,
|
|
168
|
-
geneset_name: q.geneset_name,
|
|
169
|
-
num_permutations: gseaComputeArg.num_permutations
|
|
170
|
-
};
|
|
171
|
-
const time1 = (/* @__PURE__ */ new Date()).valueOf();
|
|
172
|
-
const gsea_output = await run_python("gsea.py", "/" + JSON.stringify(genesetenrichment_input));
|
|
173
|
-
mayLog("Time taken to run blitzgsea:", formatElapsedTime(Date.now() - time1));
|
|
174
|
-
let result;
|
|
175
|
-
let data_found = false;
|
|
176
|
-
let image_found = false;
|
|
177
|
-
for (const line of gsea_output.split("\n")) {
|
|
178
|
-
if (line.startsWith("result: ")) {
|
|
179
|
-
result = JSON.parse(line.replace("result: ", ""));
|
|
180
|
-
data_found = true;
|
|
181
|
-
} else if (line.startsWith("image: ")) {
|
|
182
|
-
result = JSON.parse(line.replace("image: ", ""));
|
|
183
|
-
image_found = true;
|
|
184
|
-
} else {
|
|
185
|
-
mayLog(line);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (data_found) return result;
|
|
189
|
-
if (image_found) return path.join(cachedir_gsea, result.image_file);
|
|
190
|
-
throw new Error("data or image not found in gsea output; this should not happen");
|
|
191
|
-
}
|
|
192
224
|
export {
|
|
193
|
-
|
|
225
|
+
init
|
|
194
226
|
};
|
package/routes/grin2.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { GRIN2Payload } from "#types/checkers";
|
|
2
1
|
import serverconfig from "#src/serverconfig.js";
|
|
3
2
|
import path from "path";
|
|
4
3
|
import { run_python } from "@sjcrh/proteinpaint-python";
|
|
@@ -9,28 +8,15 @@ import { get_samples } from "#src/termdb.sql.js";
|
|
|
9
8
|
import { read_file, file_is_readable } from "#src/utils.js";
|
|
10
9
|
import { dtsnvindel, dtcnv, dtfusionrna, dtsv, dt2lesion, optionToDt, formatElapsedTime } from "#shared";
|
|
11
10
|
import { mayFilterByMaf } from "#src/mds3.init.js";
|
|
12
|
-
import
|
|
11
|
+
import { cacheOrRecompute } from "#src/utils/cacheOrRecompute.ts";
|
|
13
12
|
import { promisify } from "node:util";
|
|
14
13
|
import { exec as execCallback } from "node:child_process";
|
|
15
14
|
const MAX_LESIONS = serverconfig.features.grin2maxLesions || 25e4;
|
|
16
15
|
const GRIN2_MEMORY_BUDGET_MB = 950;
|
|
17
|
-
const GRIN2_CONCURRENCY_LIMIT =
|
|
16
|
+
const GRIN2_CONCURRENCY_LIMIT = 5;
|
|
18
17
|
const MEMORY_BASE_MB = 260;
|
|
19
18
|
const MEMORY_PER_1K_LESIONS = 2.4;
|
|
20
19
|
const MIN_LESIONS = 5e4;
|
|
21
|
-
const api = {
|
|
22
|
-
endpoint: "grin2",
|
|
23
|
-
methods: {
|
|
24
|
-
get: {
|
|
25
|
-
...GRIN2Payload,
|
|
26
|
-
init
|
|
27
|
-
},
|
|
28
|
-
post: {
|
|
29
|
-
...GRIN2Payload,
|
|
30
|
-
init
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
20
|
function init({ genomes }) {
|
|
35
21
|
return async (req, res) => {
|
|
36
22
|
const signal = req.query.__abortSignal;
|
|
@@ -51,9 +37,10 @@ function init({ genomes }) {
|
|
|
51
37
|
console.error("[GRIN2] Error stack:", e.stack);
|
|
52
38
|
const errorResponse = {
|
|
53
39
|
status: "error",
|
|
54
|
-
error: e.message || String(e)
|
|
40
|
+
error: e.message || String(e),
|
|
41
|
+
code: e.code
|
|
55
42
|
};
|
|
56
|
-
res.status(500).send(errorResponse);
|
|
43
|
+
res.status(e.status || 500).send(errorResponse);
|
|
57
44
|
}
|
|
58
45
|
};
|
|
59
46
|
}
|
|
@@ -117,11 +104,6 @@ async function runGrin2WithLimit(g, ds, request, signal) {
|
|
|
117
104
|
mayLog(`[GRIN2] Analysis complete. Active jobs: ${activeGrin2Jobs}/${GRIN2_CONCURRENCY_LIMIT}`);
|
|
118
105
|
}
|
|
119
106
|
}
|
|
120
|
-
function generateCacheFileName() {
|
|
121
|
-
const randomHex = crypto.randomBytes(16).toString("hex");
|
|
122
|
-
const cacheFileName = `grin2_results_${randomHex}.txt`;
|
|
123
|
-
return path.join(serverconfig.cachedir, "grin2", cacheFileName);
|
|
124
|
-
}
|
|
125
107
|
function buildLesionTypeMap(availableOptions) {
|
|
126
108
|
const lesionTypeMap = {};
|
|
127
109
|
for (const option of availableOptions) {
|
|
@@ -143,59 +125,22 @@ function getCnvLesionType(isGain) {
|
|
|
143
125
|
return lesionType.lesionType;
|
|
144
126
|
}
|
|
145
127
|
async function runGrin2(g, ds, request, signal) {
|
|
146
|
-
const
|
|
147
|
-
const samples = await get_samples(
|
|
148
|
-
request,
|
|
149
|
-
ds,
|
|
150
|
-
true
|
|
151
|
-
// 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
|
|
152
|
-
);
|
|
153
|
-
const cohortTime = Date.now() - startTime;
|
|
154
|
-
mayLog(`[GRIN2] Retrieved ${samples.length.toLocaleString()} samples in ${formatElapsedTime(cohortTime)}`);
|
|
155
|
-
if (samples.length === 0) {
|
|
156
|
-
throw new Error("No samples found matching the provided filter criteria");
|
|
157
|
-
}
|
|
158
|
-
const processingStartTime = Date.now();
|
|
159
|
-
const { lesions, processing } = await processSampleData(samples, ds, request);
|
|
160
|
-
if (!processing) throw new Error("Processing summary is missing");
|
|
161
|
-
const processingTime = Date.now() - processingStartTime;
|
|
162
|
-
mayLog(`[GRIN2] Data processing took ${formatElapsedTime(processingTime)}`);
|
|
163
|
-
mayLog(
|
|
164
|
-
`[GRIN2] Processing summary: ${processing?.processedSamples ?? 0}/${processing?.totalSamples ?? samples.length} samples processed successfully`
|
|
165
|
-
);
|
|
166
|
-
if (processing?.failedSamples !== void 0 && processing.failedSamples > 0) {
|
|
167
|
-
mayLog(`[GRIN2] Warning: ${processing.failedSamples} samples failed to process`);
|
|
168
|
-
}
|
|
169
|
-
if (lesions.length === 0) {
|
|
170
|
-
throw new Error("No lesions found after processing all samples. Check filter criteria and input data.");
|
|
171
|
-
}
|
|
172
|
-
const availableDataTypes = Object.keys(optionToDt).filter((key) => key in request);
|
|
173
|
-
const pyInput = {
|
|
174
|
-
genedb: path.join(serverconfig.tpmasterdir, g.genedb.dbfile),
|
|
175
|
-
chromosomelist: {},
|
|
176
|
-
lesion: JSON.stringify(lesions),
|
|
177
|
-
cacheFileName: generateCacheFileName(),
|
|
178
|
-
maxGenesToShow: request.maxGenesToShow,
|
|
179
|
-
lesionTypeMap: buildLesionTypeMap(availableDataTypes)
|
|
180
|
-
};
|
|
128
|
+
const chromosomelist = {};
|
|
181
129
|
for (const c in g.majorchr) {
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
const grin2AnalysisTime = Date.now() - grin2AnalysisStart;
|
|
193
|
-
mayLog(`[GRIN2] Python processing took ${formatElapsedTime(grin2AnalysisTime)}`);
|
|
194
|
-
const resultData = JSON.parse(pyResult);
|
|
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;
|
|
195
140
|
const rustInput = {
|
|
196
|
-
file:
|
|
141
|
+
file: cacheFile,
|
|
197
142
|
type: "grin2",
|
|
198
|
-
chrSizes:
|
|
143
|
+
chrSizes: chromosomelist,
|
|
199
144
|
plot_width: request.width,
|
|
200
145
|
plot_height: request.height,
|
|
201
146
|
device_pixel_ratio: request.devicePixelRatio,
|
|
@@ -224,8 +169,8 @@ async function runGrin2(g, ds, request, signal) {
|
|
|
224
169
|
});
|
|
225
170
|
});
|
|
226
171
|
for (const [type, data] of Object.entries(processing.lesionCounts.byType)) {
|
|
227
|
-
const { count, samples
|
|
228
|
-
lesionTypeRows.push([typeLabels[type] || type, `${count.toLocaleString()} (${
|
|
172
|
+
const { count, samples } = data;
|
|
173
|
+
lesionTypeRows.push([typeLabels[type] || type, `${count.toLocaleString()} (${samples} samples)`]);
|
|
229
174
|
}
|
|
230
175
|
}
|
|
231
176
|
const capWarningRows = [];
|
|
@@ -238,6 +183,7 @@ async function runGrin2(g, ds, request, signal) {
|
|
|
238
183
|
}
|
|
239
184
|
const response = {
|
|
240
185
|
status: "success",
|
|
186
|
+
fromCache: !freshCompute,
|
|
241
187
|
pngImg: manhattanPlotData.png,
|
|
242
188
|
plotData: manhattanPlotData.plot_data,
|
|
243
189
|
topGeneTable: resultData.topGeneTable,
|
|
@@ -248,7 +194,7 @@ async function runGrin2(g, ds, request, signal) {
|
|
|
248
194
|
rows: [
|
|
249
195
|
["Total Genes", resultData.totalGenes.toLocaleString()],
|
|
250
196
|
["Showing Top", resultData.showingTop.toLocaleString()],
|
|
251
|
-
["Cache File Name",
|
|
197
|
+
["Cache File Name", cacheFile],
|
|
252
198
|
["Total Samples", processing.totalSamples.toLocaleString()],
|
|
253
199
|
["Processed Samples", processing.processedSamples.toLocaleString()],
|
|
254
200
|
["Unprocessed Samples", (processing.unprocessedSamples ?? 0).toLocaleString()],
|
|
@@ -276,8 +222,8 @@ async function runGrin2(g, ds, request, signal) {
|
|
|
276
222
|
{
|
|
277
223
|
name: "Timing",
|
|
278
224
|
rows: [
|
|
279
|
-
["Processing", formatElapsedTime(processingTime)],
|
|
280
|
-
["GRIN2", formatElapsedTime(grin2AnalysisTime)],
|
|
225
|
+
["Processing", freshCompute ? formatElapsedTime(processingTime) : "cached"],
|
|
226
|
+
["GRIN2", freshCompute ? formatElapsedTime(grin2AnalysisTime) : "cached"],
|
|
281
227
|
["Plotting", formatElapsedTime(manhattanPlotTime)],
|
|
282
228
|
["Total", formatElapsedTime(totalTime)],
|
|
283
229
|
...capWarningRows
|
|
@@ -288,6 +234,91 @@ async function runGrin2(g, ds, request, signal) {
|
|
|
288
234
|
};
|
|
289
235
|
return response;
|
|
290
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
|
+
}
|
|
291
322
|
async function processSampleData(samples, ds, request) {
|
|
292
323
|
const lesions = [];
|
|
293
324
|
const maxLesions = await getMaxLesions();
|
|
@@ -506,5 +537,5 @@ function filterAndConvertSV(sampleName, entry, _options) {
|
|
|
506
537
|
return lesionA;
|
|
507
538
|
}
|
|
508
539
|
export {
|
|
509
|
-
|
|
540
|
+
init
|
|
510
541
|
};
|
|
@@ -1,16 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FlagStatus, SelectionPrefixes, checkSelectionType } from "#shared";
|
|
1
|
+
import { FlagStatus, SelectionPrefixes, checkSelectionType } from "#types";
|
|
3
2
|
import { getDbConnection } from "#src/aiHistoDBConnection.ts";
|
|
4
|
-
const routePath = "saveWSIAnnotation";
|
|
5
|
-
const api = {
|
|
6
|
-
endpoint: `${routePath}`,
|
|
7
|
-
methods: {
|
|
8
|
-
post: {
|
|
9
|
-
...saveWSIAnnotationPayload,
|
|
10
|
-
init
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
3
|
function init({ genomes }) {
|
|
15
4
|
return async (req, res) => {
|
|
16
5
|
try {
|
|
@@ -162,6 +151,6 @@ function validateQuery(ds, connection) {
|
|
|
162
151
|
};
|
|
163
152
|
}
|
|
164
153
|
export {
|
|
165
|
-
|
|
154
|
+
init,
|
|
166
155
|
validate_query_saveWSIAnnotation
|
|
167
156
|
};
|