@sjcrh/proteinpaint-server 2.147.2-0 → 2.148.1

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.
@@ -39,8 +39,6 @@ function termdb_test_default() {
39
39
  displaySampleIds: () => true,
40
40
  // allow to display sample-level data
41
41
  timeUnit: "years",
42
- minTimeSinceDx: 5,
43
- // enrollment in sjlife requires 5 years since cancer diagnosis
44
42
  ageEndOffset: 274e-5,
45
43
  // number of years to offset ending age of patients
46
44
  // for cox outcome with timeScale='age'
@@ -108,7 +106,6 @@ function termdb_test_default() {
108
106
  plotConfigByCohort: {
109
107
  default: {
110
108
  report: {
111
- filterTWs: [{ id: "diaggrp" }],
112
109
  sections: [
113
110
  {
114
111
  name: "Demographics",
@@ -332,7 +329,6 @@ function termdb_test_default() {
332
329
  },
333
330
  rnaseqGeneCount: {
334
331
  storage_type: "HDF5",
335
- newformat: true,
336
332
  file: "files/hg38/TermdbTest/rnaseq/TermdbTest.geneCounts.new.h5"
337
333
  },
338
334
  singleCell: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.147.2-0",
3
+ "version": "2.148.1",
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",
@@ -23,7 +23,7 @@
23
23
  "combined:coverage": "coverageKey=test c8 --all --src=proteinpaint/server --experimental-monocart -r=v8 -r=html -r=json -r=markdown-summary -r=markdown-details -o=./.coverage tsx ./coverage.js & ",
24
24
  "postcombined:coverage": "rm -rf ./cache",
25
25
  "spec:coverage": "tsx test/relevant.js",
26
- "getconf": "../build/getConfigProp.js",
26
+ "getconf": "../build/getConfigProp.cjs",
27
27
  "doc": "../augen/build.sh routes shared/types/routes shared/checkers ../public/docs/server",
28
28
  "mjs": "esbuild \"$DIR/*.ts\" --platform=node --outdir=\"$DIR\" --format=esm",
29
29
  "cjs": "esbuild \"$DIR/*.ts\" --platform=node --outdir=\"$DIR\" --format=cjs",
@@ -63,9 +63,9 @@
63
63
  "@sjcrh/augen": "2.143.0",
64
64
  "@sjcrh/proteinpaint-python": "2.146.0",
65
65
  "@sjcrh/proteinpaint-r": "2.146.4-0",
66
- "@sjcrh/proteinpaint-rust": "2.146.4-1",
66
+ "@sjcrh/proteinpaint-rust": "2.148.1",
67
67
  "@sjcrh/proteinpaint-shared": "2.147.1",
68
- "@sjcrh/proteinpaint-types": "2.147.2-0",
68
+ "@sjcrh/proteinpaint-types": "2.148.1",
69
69
  "@types/express": "^5.0.0",
70
70
  "@types/express-session": "^1.18.1",
71
71
  "better-sqlite3": "^9.4.1",
@@ -35,7 +35,11 @@ function init({ genomes }) {
35
35
  wsimage.activePatchColor = ds.queries?.WSImages?.activePatchColor;
36
36
  if (ds.queries.WSImages.getWSIPredictionPatches) {
37
37
  const predictions = await ds.queries.WSImages.getWSIPredictionPatches(projectId, wsimageFilename);
38
- wsimage.predictions = predictions;
38
+ const classMap = new Map((wsimage.classes || []).map((c) => [c.id, c.label]));
39
+ wsimage.predictions = (predictions || []).map((p) => {
40
+ const label = classMap.get(p.class) ?? p.class;
41
+ return { ...p, class: label };
42
+ });
39
43
  }
40
44
  wsimages.push(wsimage);
41
45
  }
@@ -42,6 +42,7 @@ function init({ genomes }) {
42
42
  status: "error",
43
43
  error: "No retraining script defined"
44
44
  });
45
+ return;
45
46
  }
46
47
  res.status(200).send({
47
48
  status: "ok",
package/routes/grin2.js CHANGED
@@ -6,6 +6,7 @@ import { mayLog } from "#src/helpers.ts";
6
6
  import { get_samples } from "#src/termdb.sql.js";
7
7
  import { read_file, file_is_readable } from "#src/utils.js";
8
8
  import { dtsnvindel, dtcnv, dtfusionrna } from "#shared/common.js";
9
+ const MAX_LESIONS_PER_TYPE = 5e4;
9
10
  const api = {
10
11
  endpoint: "grin2",
11
12
  methods: {
@@ -23,7 +24,6 @@ function init({ genomes }) {
23
24
  return async (req, res) => {
24
25
  try {
25
26
  const request = req.query;
26
- console.log("[GRIN2] request:", request);
27
27
  const g = genomes[request.genome];
28
28
  if (!g) throw new Error("genome missing");
29
29
  const ds = g.datasets?.[request.dslabel];
@@ -43,7 +43,6 @@ function init({ genomes }) {
43
43
  }
44
44
  async function runGrin2(g, ds, request) {
45
45
  const startTime = Date.now();
46
- mayLog("[GRIN2] Getting samples from cohort filter...");
47
46
  const samples = await get_samples(
48
47
  request,
49
48
  ds,
@@ -55,16 +54,16 @@ async function runGrin2(g, ds, request) {
55
54
  if (samples.length === 0) {
56
55
  throw new Error("No samples found matching the provided filter criteria");
57
56
  }
58
- mayLog("[GRIN2] Processing sample data...");
57
+ const tracker = getLesionTracker(request);
59
58
  const processingStartTime = Date.now();
60
- const { lesions, processingSummary } = await processSampleData(samples, ds, request);
59
+ const { lesions, processingSummary } = await processSampleData(samples, ds, request, tracker);
61
60
  const processingTime = Date.now() - processingStartTime;
62
61
  const processingTimeToPrint = Math.round(processingTime / 1e3);
63
62
  mayLog(`[GRIN2] Data processing took ${processingTimeToPrint} seconds`);
64
63
  mayLog(
65
- `[GRIN2] Processing summary: ${processingSummary?.successfulSamples ?? 0}/${processingSummary?.totalSamples ?? samples.length} samples processed successfully`
64
+ `[GRIN2] Processing summary: ${processingSummary?.processedSamples ?? 0}/${processingSummary?.totalSamples ?? samples.length} samples processed successfully`
66
65
  );
67
- if (processingSummary && processingSummary.failedSamples > 0) {
66
+ if (processingSummary?.failedSamples !== void 0 && processingSummary.failedSamples > 0) {
68
67
  mayLog(`[GRIN2] Warning: ${processingSummary.failedSamples} samples failed to process`);
69
68
  }
70
69
  if (lesions.length === 0) {
@@ -79,14 +78,12 @@ async function runGrin2(g, ds, request) {
79
78
  width: request.width,
80
79
  height: request.height
81
80
  };
82
- mayLog("[GRIN2] Prepared input for Python script:", { ...pyInput });
83
81
  for (const c in g.majorchr) {
84
82
  if (ds.queries.singleSampleMutation.discoPlot?.skipChrM) {
85
83
  if (c.toLowerCase() == "chrm") continue;
86
84
  }
87
85
  pyInput.chromosomelist[c] = g.majorchr[c];
88
86
  }
89
- mayLog(`[GRIN2] Prepared ${lesions.length.toLocaleString()} lesions for analysis`);
90
87
  const grin2AnalysisStart = Date.now();
91
88
  const pyResult = await run_python("grin2PpWrapper.py", JSON.stringify(pyInput));
92
89
  if (pyResult.stderr?.trim()) {
@@ -119,75 +116,137 @@ async function runGrin2(g, ds, request) {
119
116
  };
120
117
  return response;
121
118
  }
122
- async function processSampleData(samples, ds, request) {
119
+ function getLesionTracker(req) {
120
+ const currentTypes = [];
121
+ if (req.snvindelOptions) currentTypes.push(dtsnvindel);
122
+ if (req.cnvOptions) currentTypes.push(dtcnv);
123
+ if (req.fusionOptions) currentTypes.push(dtfusionrna);
124
+ const track = /* @__PURE__ */ new Map();
125
+ for (const t of currentTypes) track.set(t, { count: 0 });
126
+ return track;
127
+ }
128
+ function allTypesCapped(tracker) {
129
+ for (const value of tracker.values()) {
130
+ if (value.count < MAX_LESIONS_PER_TYPE) return false;
131
+ }
132
+ return true;
133
+ }
134
+ async function processSampleData(samples, ds, request, tracker) {
123
135
  const lesions = [];
124
- let lesionId = 1;
125
136
  const processingSummary = {
126
137
  totalSamples: samples.length,
127
- successfulSamples: 0,
138
+ processedSamples: 0,
128
139
  failedSamples: 0,
129
- failedFiles: []
140
+ failedFiles: [],
141
+ totalLesions: 0,
142
+ processedLesions: 0,
143
+ unprocessedSamples: 0
130
144
  };
131
- mayLog(`[GRIN2] Processing JSON files for ${samples.length.toLocaleString()} samples`);
132
- for (const sample of samples) {
145
+ outer: for (let i = 0; i < samples.length; i++) {
146
+ if (allTypesCapped(tracker)) {
147
+ const remaining = samples.length - i;
148
+ if (remaining > 0) processingSummary.unprocessedSamples += remaining;
149
+ mayLog("[GRIN2] All enabled per-type caps reached; stopping early.");
150
+ break outer;
151
+ }
152
+ const sample = samples[i];
153
+ const filepath = path.join(serverconfig.tpmasterdir, ds.queries.singleSampleMutation.folder, sample.name);
133
154
  try {
134
- const filepath = path.join(serverconfig.tpmasterdir, ds.queries.singleSampleMutation.folder, sample.name);
135
155
  await file_is_readable(filepath);
136
156
  const mlst = JSON.parse(await read_file(filepath));
137
- const { sampleLesions } = await processSampleMlst(sample.name, mlst, lesionId, request);
157
+ const { sampleLesions } = await processSampleMlst(sample.name, mlst, request, tracker);
138
158
  lesions.push(...sampleLesions);
139
- lesionId += sampleLesions.length;
140
- processingSummary.successfulSamples++;
159
+ processingSummary.processedSamples += 1;
160
+ processingSummary.totalLesions += sampleLesions.length;
161
+ if (allTypesCapped(tracker)) {
162
+ const remaining = samples.length - 1 - i;
163
+ if (remaining > 0) processingSummary.unprocessedSamples += remaining;
164
+ mayLog("[GRIN2] All enabled per-type caps reached; stopping early.");
165
+ break outer;
166
+ }
141
167
  } catch (error) {
142
- processingSummary.failedSamples++;
143
- processingSummary.failedFiles?.push(sample.name);
168
+ processingSummary.failedSamples += 1;
169
+ processingSummary.failedFiles.push({
170
+ sampleName: sample.name,
171
+ filePath: filepath,
172
+ error: error instanceof Error ? error.message || "Unknown error" : String(error)
173
+ });
174
+ mayLog(`[GRIN2] Error processing sample ${sample.name}`);
175
+ }
176
+ }
177
+ processingSummary.processedLesions = lesions.length;
178
+ for (const [type, info] of tracker.entries()) {
179
+ if (info.count >= MAX_LESIONS_PER_TYPE) {
180
+ let label;
181
+ switch (type) {
182
+ case dtsnvindel:
183
+ label = "mutation";
184
+ break;
185
+ case dtcnv:
186
+ label = "CNV (gain/loss)";
187
+ break;
188
+ case dtfusionrna:
189
+ label = "fusion";
190
+ break;
191
+ default:
192
+ label = `type ${type}`;
193
+ break;
194
+ }
144
195
  mayLog(
145
- `[GRIN2] Error processing sample ${sample.name}: ${typeof error === "object" && error !== null && "message" in error ? error.message : String(error)}`
196
+ `[GRIN2] Warning: ${label} lesions reached the per-type cap of ${MAX_LESIONS_PER_TYPE.toLocaleString()}. No further ${label} lesions were processed.`
146
197
  );
147
198
  }
148
199
  }
149
- mayLog(`[GRIN2] Total lesions processed: ${lesions.length.toLocaleString()}`);
150
- return {
151
- lesions,
152
- processingSummary
153
- };
200
+ return { lesions, processingSummary };
154
201
  }
155
- async function processSampleMlst(sampleName, mlst, startId, request) {
156
- const lesions = [];
202
+ async function processSampleMlst(sampleName, mlst, request, tracker) {
203
+ const sampleLesions = [];
157
204
  for (const m of mlst) {
158
205
  switch (m.dt) {
159
206
  case dtsnvindel: {
160
207
  if (!request.snvindelOptions) break;
161
- const snvIndelLesion = filterAndConvertSnvIndel(sampleName, m, request.snvindelOptions);
162
- if (snvIndelLesion) {
163
- lesions.push(snvIndelLesion);
208
+ const entry = tracker.get(dtsnvindel);
209
+ if (entry && entry.count >= MAX_LESIONS_PER_TYPE) {
210
+ break;
211
+ }
212
+ const les = filterAndConvertSnvIndel(sampleName, m, request.snvindelOptions);
213
+ if (les && entry) {
214
+ entry.count++;
215
+ sampleLesions.push(les);
164
216
  }
165
217
  break;
166
218
  }
167
219
  case dtcnv: {
168
220
  if (!request.cnvOptions) break;
169
- const cnvLesion = filterAndConvertCnv(sampleName, m, request.cnvOptions);
170
- if (cnvLesion) {
171
- lesions.push(cnvLesion);
221
+ const cnv = tracker.get(dtcnv);
222
+ if (cnv && cnv.count >= MAX_LESIONS_PER_TYPE) {
223
+ break;
224
+ }
225
+ const les = filterAndConvertCnv(sampleName, m, request.cnvOptions);
226
+ if (les && cnv) {
227
+ cnv.count++;
228
+ sampleLesions.push(les);
172
229
  }
173
230
  break;
174
231
  }
175
232
  case dtfusionrna: {
176
233
  if (!request.fusionOptions) break;
177
- const fusionLesion = filterAndConvertFusion(sampleName, m, request.fusionOptions);
178
- if (fusionLesion) {
179
- lesions.push(fusionLesion);
234
+ const fusion = tracker.get(dtfusionrna);
235
+ if (fusion && fusion.count >= MAX_LESIONS_PER_TYPE) {
236
+ break;
237
+ }
238
+ const les = filterAndConvertFusion(sampleName, m, request.fusionOptions);
239
+ if (les && fusion) {
240
+ fusion.count++;
241
+ sampleLesions.push(les);
180
242
  }
181
243
  break;
182
244
  }
183
- default: {
245
+ default:
184
246
  break;
185
- }
186
247
  }
187
248
  }
188
- return {
189
- sampleLesions: lesions
190
- };
249
+ return { sampleLesions };
191
250
  }
192
251
  function filterAndConvertSnvIndel(sampleName, entry, options) {
193
252
  if (!options?.consequences) {
@@ -204,12 +204,7 @@ async function run_DE(param, ds, term_results, term_results2) {
204
204
  const sample_size_limit = 8;
205
205
  if (group1names.length <= sample_size_limit && group2names.length <= sample_size_limit || param.method == "edgeR" || param.method == "limma") {
206
206
  const time12 = (/* @__PURE__ */ new Date()).valueOf();
207
- let result2;
208
- if (q.newformat) {
209
- result2 = JSON.parse(await run_R("edge_newh5.R", JSON.stringify(expression_input)));
210
- } else {
211
- result2 = JSON.parse(await run_R("edge.R", JSON.stringify(expression_input)));
212
- }
207
+ const result2 = JSON.parse(await run_R("edge_newh5.R", JSON.stringify(expression_input)));
213
208
  mayLog("Time taken to run edgeR:", formatElapsedTime(Date.now() - time12));
214
209
  param.method = "edgeR";
215
210
  const ql_imagePath = path.join(serverconfig.cachedir, result2.edgeR_ql_image_name[0]);
@@ -259,30 +254,18 @@ async function validate_query_rnaseqGeneCount(ds) {
259
254
  if (ds.queries.rnaseqGeneCount.storage_type == "text") {
260
255
  samples = (await get_header_txt(q.file, null)).split(" ").slice(4);
261
256
  } else if (ds.queries.rnaseqGeneCount.storage_type == "HDF5") {
262
- if (q.newformat) {
263
- const get_samples_from_hdf5 = {
264
- hdf5_file: q.file,
265
- validate: true
266
- };
267
- const time1 = (/* @__PURE__ */ new Date()).valueOf();
268
- const result = await run_rust("readH5", JSON.stringify(get_samples_from_hdf5));
269
- const time2 = (/* @__PURE__ */ new Date()).valueOf();
270
- mayLog("Time taken to query gene expression:", time2 - time1, "ms");
271
- const vr = JSON.parse(result);
272
- if (vr.status !== "success") throw vr.message;
273
- if (!Array.isArray(vr.samples)) throw "HDF5 file has no samples, please check file.";
274
- samples = vr.samples;
275
- } else {
276
- const get_samples_from_hdf5 = {
277
- input_file: q.file,
278
- data_type: "get_samples"
279
- };
280
- const time1 = (/* @__PURE__ */ new Date()).valueOf();
281
- const result = await run_rust("DEanalysis", JSON.stringify(get_samples_from_hdf5));
282
- const time2 = (/* @__PURE__ */ new Date()).valueOf();
283
- mayLog("Time taken to query gene expression:", time2 - time1, "ms");
284
- samples = result.split(",");
285
- }
257
+ const get_samples_from_hdf5 = {
258
+ hdf5_file: q.file,
259
+ validate: true
260
+ };
261
+ const time1 = (/* @__PURE__ */ new Date()).valueOf();
262
+ const result = await run_rust("readH5", JSON.stringify(get_samples_from_hdf5));
263
+ const time2 = (/* @__PURE__ */ new Date()).valueOf();
264
+ mayLog("Time taken to query gene expression:", time2 - time1, "ms");
265
+ const vr = JSON.parse(result);
266
+ if (vr.status !== "success") throw vr.message;
267
+ if (!Array.isArray(vr.samples)) throw "HDF5 file has no samples, please check file.";
268
+ samples = vr.samples;
286
269
  } else throw "unknown storage type:" + ds.queries.rnaseqGeneCount.storage_type;
287
270
  q.allSampleSet = new Set(samples);
288
271
  const unknownSamples = [];