@sjcrh/proteinpaint-server 2.188.1 → 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.
Files changed (82) hide show
  1. package/package.json +9 -12
  2. package/routes/aiProjectAdmin.js +2 -28
  3. package/routes/aiProjectSelectedWSImages.js +2 -17
  4. package/routes/brainImaging.js +1 -15
  5. package/routes/brainImagingSamples.js +1 -15
  6. package/routes/burden.js +1 -15
  7. package/routes/correlationVolcano.js +1 -15
  8. package/routes/dataset.js +1 -16
  9. package/routes/deleteWSITileSelection.js +2 -12
  10. package/routes/dsdata.js +1 -19
  11. package/routes/gdc.grin2.list.js +1 -15
  12. package/routes/gdc.grin2.run.js +1 -15
  13. package/routes/gdc.maf.js +1 -15
  14. package/routes/gdc.mafBuild.js +1 -15
  15. package/routes/genesetEnrichment.js +129 -97
  16. package/routes/grin2.js +110 -79
  17. package/routes/saveWSIAnnotation.js +2 -13
  18. package/routes/termdb.DE.js +137 -54
  19. package/routes/termdb.categories.js +2 -16
  20. package/routes/termdb.chat.js +169 -1076
  21. package/routes/termdb.cluster.js +5 -16
  22. package/routes/termdb.config.js +12 -17
  23. package/routes/termdb.descrstats.js +2 -16
  24. package/routes/termdb.diffMeth.js +100 -21
  25. package/routes/termdb.geneRanking.js +139 -0
  26. package/routes/termdb.proteome.js +1 -15
  27. package/routes/termdb.runChart.js +16 -30
  28. package/routes/termdb.sampleScatter.js +7 -97
  29. package/routes/termdb.singleCellPlots.js +159 -0
  30. package/routes/termdb.singlecellSamples.js +6 -16
  31. package/routes/termdb.violinBox.js +1 -15
  32. package/routes/wsimages.js +1 -16
  33. package/src/app.js +4014 -4127
  34. package/routes/_template_.js +0 -33
  35. package/routes/aiProjectTrainModel.js +0 -68
  36. package/routes/alphaGenome.js +0 -41
  37. package/routes/alphaGenomeTypes.js +0 -36
  38. package/routes/dzimages.js +0 -55
  39. package/routes/gene2canonicalisoform.js +0 -37
  40. package/routes/genelookup.js +0 -32
  41. package/routes/genesetOverrepresentation.js +0 -49
  42. package/routes/genomes.js +0 -150
  43. package/routes/healthcheck.js +0 -35
  44. package/routes/hicdata.js +0 -74
  45. package/routes/hicgenome.js +0 -75
  46. package/routes/hicstat.js +0 -35
  47. package/routes/img.js +0 -46
  48. package/routes/isoformlst.js +0 -48
  49. package/routes/ntseq.js +0 -36
  50. package/routes/pdomain.js +0 -53
  51. package/routes/profile.barchart2.js +0 -114
  52. package/routes/profile.forms2.js +0 -107
  53. package/routes/profile.polar2.js +0 -101
  54. package/routes/profile.radar2.js +0 -112
  55. package/routes/profile.radarFacility2.js +0 -148
  56. package/routes/sampledzimages.js +0 -48
  57. package/routes/samplewsimages.js +0 -60
  58. package/routes/snp.js +0 -98
  59. package/routes/termdb.chat2.js +0 -217
  60. package/routes/termdb.chat3.js +0 -209
  61. package/routes/termdb.cohort.summary.js +0 -37
  62. package/routes/termdb.cohorts.js +0 -41
  63. package/routes/termdb.dapVolcano.js +0 -80
  64. package/routes/termdb.dmr.js +0 -93
  65. package/routes/termdb.filterTermValues.js +0 -89
  66. package/routes/termdb.isoformAvailability.js +0 -35
  67. package/routes/termdb.numericcategories.js +0 -46
  68. package/routes/termdb.percentile.js +0 -66
  69. package/routes/termdb.profileFormScores.js +0 -92
  70. package/routes/termdb.profileScores.js +0 -113
  71. package/routes/termdb.rootterm.js +0 -39
  72. package/routes/termdb.sampleImages.js +0 -63
  73. package/routes/termdb.singleSampleMutation.js +0 -75
  74. package/routes/termdb.singlecellDEgenes.js +0 -55
  75. package/routes/termdb.singlecellData.js +0 -39
  76. package/routes/termdb.termchildren.js +0 -42
  77. package/routes/termdb.termsbyids.js +0 -50
  78. package/routes/termdb.topMutatedGenes.js +0 -127
  79. package/routes/termdb.topTermsByType.js +0 -96
  80. package/routes/termdb.topVariablyExpressedGenes.js +0 -132
  81. package/routes/tileserver.js +0 -68
  82. 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 { readCacheFileOrRecompute, stableStringify } from "#src/diffAnalysis.ts";
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 computeGSEA({ q, genomes });
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 result = await readCacheFileOrRecompute({
86
- daRequest: q.daRequest,
87
- genomes
88
- });
89
- if (result.cacheId !== q.cacheId) throw new Error("cacheId does not match daRequest");
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: result.geneData.map((g) => g.gene_name),
93
- fold_change: result.geneData.map((g) => g.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
- if (result.kind === "DM") {
97
- return {
98
- genes: result.promoterData.map((p) => p.gene_name),
99
- fold_change: result.promoterData.map((p) => p.fold_change)
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
- api
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 crypto from "crypto";
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 = 10;
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 startTime = Date.now();
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
- pyInput.chromosomelist[c] = g.majorchr[c];
183
- }
184
- const grin2AnalysisStart = Date.now();
185
- const pyResult = await run_python("grin2PpWrapper.py", JSON.stringify(pyInput), { signal });
186
- if (pyResult.stderr?.trim()) {
187
- mayLog(`[GRIN2] Python stderr: ${pyResult.stderr}`);
188
- if (pyResult.stderr.includes("ERROR:")) {
189
- throw new Error(`Python script error: ${pyResult.stderr}`);
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: resultData.cacheFileName,
141
+ file: cacheFile,
197
142
  type: "grin2",
198
- chrSizes: pyInput.chromosomelist,
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: samples2 } = data;
228
- lesionTypeRows.push([typeLabels[type] || type, `${count.toLocaleString()} (${samples2} samples)`]);
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", resultData.cacheFileName],
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
- api
540
+ init
510
541
  };
@@ -1,16 +1,5 @@
1
- import { saveWSIAnnotationPayload } from "#types/checkers";
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
- api,
154
+ init,
166
155
  validate_query_saveWSIAnnotation
167
156
  };