@sjcrh/proteinpaint-server 2.145.1 → 2.146.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.145.1",
3
+ "version": "2.146.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",
@@ -61,11 +61,11 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@sjcrh/augen": "2.143.0",
64
- "@sjcrh/proteinpaint-python": "2.144.0",
64
+ "@sjcrh/proteinpaint-python": "2.146.0",
65
65
  "@sjcrh/proteinpaint-r": "2.145.1-0",
66
- "@sjcrh/proteinpaint-rust": "2.145.1",
67
- "@sjcrh/proteinpaint-shared": "2.145.1-0",
68
- "@sjcrh/proteinpaint-types": "2.145.1-1",
66
+ "@sjcrh/proteinpaint-rust": "2.146.0",
67
+ "@sjcrh/proteinpaint-shared": "2.145.2",
68
+ "@sjcrh/proteinpaint-types": "2.146.0",
69
69
  "@types/express": "^5.0.0",
70
70
  "@types/express-session": "^1.18.1",
71
71
  "better-sqlite3": "^9.4.1",
@@ -82,9 +82,7 @@
82
82
  "got": "^14.2.0",
83
83
  "image-size": "^0.5.5",
84
84
  "jsonwebtoken": "^9.0.0",
85
- "jstat": "^1.9.3",
86
85
  "ky": "^1.2.1",
87
- "lazy": "^1.0.11",
88
86
  "micromatch": "^4.0.5",
89
87
  "minimatch": "^10.0.1",
90
88
  "node-fetch": "^2.6.1",
@@ -51,9 +51,6 @@ function init({ genomes }) {
51
51
  };
52
52
  });
53
53
  }
54
- if (ds.queries.WSImages.makeGeoJson) {
55
- await ds.queries.WSImages.makeGeoJson(projectId, wsimageFilename);
56
- }
57
54
  wsimages.push(wsimage);
58
55
  }
59
56
  }
@@ -2,9 +2,7 @@ import { boxplotPayload } from "#types/checkers";
2
2
  import { getData } from "../src/termdb.matrix.js";
3
3
  import { boxplot_getvalue } from "../src/utils.js";
4
4
  import { sortPlot2Values } from "./termdb.violin.ts";
5
- import { roundValueAuto } from "#shared/roundValue.js";
6
- import { getMean, getVariance } from "#shared/descriptive.stats.js";
7
- const minSampleSize = 5;
5
+ import { summaryStats, getDescriptiveStats, summaryStatsFromStats } from "#shared/descriptive.stats.js";
8
6
  const api = {
9
7
  endpoint: "termdb/boxplot",
10
8
  methods: {
@@ -31,6 +29,9 @@ function init({ genomes }) {
31
29
  if (q.divideTw) terms.push(q.divideTw);
32
30
  const data = await getData({ filter: q.filter, filter0: q.filter0, terms, __protected__: q.__protected__ }, ds);
33
31
  if (data.error) throw data.error;
32
+ const samples = Object.values(data.samples);
33
+ const values = samples.map((s) => s?.[q.tw.$id]?.value).filter((v) => typeof v === "number");
34
+ const statsAllSummary = summaryStats(values).values;
34
35
  const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
35
36
  const overlayTerm = q.overlayTw;
36
37
  const divideTerm = q.divideTw;
@@ -45,24 +46,30 @@ function init({ genomes }) {
45
46
  if (!absMin && absMin !== 0) throw "absMin is undefined [termdb.boxplot init()]";
46
47
  if (!absMax && absMax !== 0) throw "absMax is undefined [termdb.boxplot init()]";
47
48
  const charts = {};
49
+ let outlierMin = Number.POSITIVE_INFINITY, outlierMax = Number.NEGATIVE_INFINITY;
48
50
  for (const [chart, plot2values] of chart2plot2values) {
49
51
  const plots = [];
50
- for (const [key, values] of sortPlot2Values(data, plot2values, overlayTerm)) {
51
- const sortedValues = values.sort((a, b) => a - b);
52
+ for (const [key, values2] of sortPlot2Values(data, plot2values, overlayTerm)) {
53
+ const sortedValues = values2.sort((a, b) => a - b);
52
54
  const vs = sortedValues.map((v) => {
53
55
  const value = { value: v };
54
56
  return value;
55
57
  });
56
- const boxplot = boxplot_getvalue(vs);
58
+ const stats = getDescriptiveStats(sortedValues);
59
+ if (q.removeOutliers) {
60
+ outlierMin = Math.min(outlierMin, stats.outlierMin);
61
+ outlierMax = Math.max(outlierMax, stats.outlierMax);
62
+ }
63
+ const descrStats = summaryStatsFromStats(stats, true).values;
64
+ const boxplot = boxplot_getvalue(vs, q.removeOutliers);
57
65
  if (!boxplot) throw "boxplot_getvalue failed [termdb.boxplot init()]";
58
- const descrStats = setDescrStats(boxplot, sortedValues);
59
66
  const _plot = {
60
67
  boxplot,
61
68
  descrStats
62
69
  };
63
70
  if (overlayTerm) {
64
71
  const _key = overlayTerm?.term?.values?.[key]?.label || key;
65
- const plotLabel = `${_key}, n=${values.length}`;
72
+ const plotLabel = `${_key}, n=${values2.length}`;
66
73
  const overlayBins = numericBins(overlayTerm, data);
67
74
  const plot = Object.assign(_plot, {
68
75
  color: overlayTerm?.term?.values?.[key]?.color || null,
@@ -73,7 +80,7 @@ function init({ genomes }) {
73
80
  plot.boxplot.label = plotLabel;
74
81
  plots.push(plot);
75
82
  } else {
76
- const plotLabel = `${sampleType}, n=${values.length}`;
83
+ const plotLabel = `${sampleType}, n=${values2.length}`;
77
84
  const plot = Object.assign(_plot, {
78
85
  key: sampleType
79
86
  });
@@ -89,10 +96,11 @@ function init({ genomes }) {
89
96
  charts[chart] = { chartId: chart, plots };
90
97
  }
91
98
  const returnData = {
92
- absMin,
93
- absMax,
99
+ absMin: q.removeOutliers ? outlierMin : absMin,
100
+ absMax: q.removeOutliers ? outlierMax : absMax,
94
101
  charts,
95
- uncomputableValues: setUncomputableValues(uncomputableValues)
102
+ uncomputableValues: setUncomputableValues(uncomputableValues),
103
+ descrStats: statsAllSummary
96
104
  };
97
105
  res.send(returnData);
98
106
  } catch (e) {
@@ -114,24 +122,6 @@ function setHiddenPlots(term, plots) {
114
122
  }
115
123
  return plots;
116
124
  }
117
- function setDescrStats(boxplot, sortedValues) {
118
- if (sortedValues.length < minSampleSize) return [{ id: "total", label: "Total", value: sortedValues.length }];
119
- const mean = getMean(sortedValues);
120
- const variance = getVariance(sortedValues);
121
- const sd = Math.sqrt(variance);
122
- return [
123
- { id: "total", label: "Total", value: sortedValues.length },
124
- { id: "min", label: "Minimum", value: roundValueAuto(sortedValues[0], true) },
125
- { id: "p25", label: "1st quartile", value: roundValueAuto(boxplot.p25, true) },
126
- { id: "median", label: "Median", value: roundValueAuto(boxplot.p50, true) },
127
- { id: "mean", label: "Mean", value: roundValueAuto(mean, true) },
128
- { id: "p75", label: "3rd quartile", value: roundValueAuto(boxplot.p75, true) },
129
- { id: "max", label: "Maximum", value: roundValueAuto(sortedValues[sortedValues.length - 1], true) },
130
- { id: "sd", label: "Standard deviation", value: isNaN(sd) ? null : roundValueAuto(sd, true) },
131
- { id: "variance", label: "Variance", value: roundValueAuto(variance, true) },
132
- { id: "iqr", label: "Inter-quartile range", value: roundValueAuto(boxplot.iqr, true) }
133
- ];
134
- }
135
125
  function setUncomputableValues(values) {
136
126
  if (Object.entries(values)?.length) {
137
127
  return Object.entries(values).map(([label, v]) => ({ label, value: v }));
@@ -0,0 +1,71 @@
1
+ import { ChatPayload } from "#types/checkers";
2
+ import { run_rust } from "@sjcrh/proteinpaint-rust";
3
+ import serverconfig from "../src/serverconfig.js";
4
+ import { mayLog } from "#src/helpers.ts";
5
+ const api = {
6
+ endpoint: "termdb/chat",
7
+ methods: {
8
+ get: {
9
+ ...ChatPayload,
10
+ init
11
+ },
12
+ post: {
13
+ ...ChatPayload,
14
+ init
15
+ }
16
+ }
17
+ };
18
+ function init({ genomes }) {
19
+ return async (req, res) => {
20
+ const q = req.query;
21
+ try {
22
+ const g = genomes[q.genome];
23
+ if (!g) throw "invalid genome";
24
+ const ds = g.datasets?.[q.dslabel];
25
+ if (!ds) throw "invalid dslabel";
26
+ let apilink;
27
+ let comp_model_name;
28
+ let embedding_model_name;
29
+ if (serverconfig.llm_backend == "SJ") {
30
+ apilink = serverconfig.sj_apilink;
31
+ comp_model_name = serverconfig.sj_comp_model_name;
32
+ embedding_model_name = serverconfig.sj_embedding_model_name;
33
+ } else if (serverconfig.llm_backend == "ollama") {
34
+ apilink = serverconfig.ollama_apilink;
35
+ comp_model_name = serverconfig.ollama_comp_model_name;
36
+ embedding_model_name = serverconfig.ollama_embedding_model_name;
37
+ } else {
38
+ throw "llm_backend either needs to be 'SJ' or 'ollama'";
39
+ }
40
+ const chatbot_input = {
41
+ // Just hardcoding variables here, these will later be defined in more appropriate places
42
+ user_input: q.prompt,
43
+ apilink,
44
+ dataset_db: serverconfig.tpmasterdir + "/" + ds.cohort.db.file,
45
+ comp_model_name,
46
+ embedding_model_name,
47
+ llm_backend_name: serverconfig.llm_backend
48
+ // The type of backend (engine) used for running the embedding and completion model. Currently "SJ" and "Ollama" are supported
49
+ };
50
+ const time1 = (/* @__PURE__ */ new Date()).valueOf();
51
+ const ai_output_data = await run_rust("aichatbot", JSON.stringify(chatbot_input));
52
+ const time2 = (/* @__PURE__ */ new Date()).valueOf();
53
+ mayLog("Time taken to run rust AI chatbot:", time2 - time1, "ms");
54
+ let ai_output_json = "";
55
+ for (const line of ai_output_data.split("\n")) {
56
+ if (line.startsWith("final_output:") == true) {
57
+ ai_output_json = JSON.parse(line.replace("final_output:", ""));
58
+ } else {
59
+ mayLog(line);
60
+ }
61
+ }
62
+ res.send(ai_output_json);
63
+ } catch (e) {
64
+ if (e.stack) console.log(e.stack);
65
+ res.send({ error: e?.message || e });
66
+ }
67
+ };
68
+ }
69
+ export {
70
+ api
71
+ };
@@ -1,6 +1,7 @@
1
1
  import { termdbCohortSummaryPayload } from "#types/checkers";
2
2
  import { get_ds_tdb } from "#src/termdb.js";
3
3
  import { mayCopyFromCookie } from "#src/utils.js";
4
+ import { get_samples } from "#src/termdb.sql.js";
4
5
  const api = {
5
6
  endpoint: "termdb/cohort/summary",
6
7
  methods: {
@@ -18,7 +19,11 @@ function init({ genomes }) {
18
19
  const genome = genomes[q.genome];
19
20
  if (!genome) throw "invalid genome";
20
21
  const [ds] = get_ds_tdb(genome, q);
21
- const count = ds.cohort.termdb.q?.getCohortSampleCount?.(q.cohort) || 1;
22
+ let count;
23
+ if (ds.cohort.termdb.getAdditionalFilter) {
24
+ const samples = await get_samples(q, ds);
25
+ count = samples.length;
26
+ } else count = ds.cohort.termdb.q?.getCohortSampleCount?.(q.cohort) || 1;
22
27
  res.send({ count });
23
28
  } catch (e) {
24
29
  res.send({ error: e.message || e });
@@ -134,15 +134,9 @@ function addNonDictionaryQueries(c, ds, genome) {
134
134
  };
135
135
  if (q.snvindel.byisoform?.processTwsInOneQuery) q2.snvindel.byisoform = { processTwsInOneQuery: true };
136
136
  }
137
- if (q.trackLst) {
138
- q2.trackLst = q.trackLst;
139
- }
140
137
  if (q.svfusion) {
141
138
  q2.svfusion = {};
142
139
  }
143
- if (q.ld) {
144
- q2.ld = JSON.parse(JSON.stringify(q.ld));
145
- }
146
140
  if (q.cnv) {
147
141
  q2.cnv = {};
148
142
  for (const k of [
@@ -177,6 +171,15 @@ function addNonDictionaryQueries(c, ds, genome) {
177
171
  if (q.geneExpression) {
178
172
  q2.geneExpression = { unit: q.geneExpression.unit };
179
173
  }
174
+ if (q.ld) {
175
+ q2.ld = structuredClone(q.ld);
176
+ }
177
+ if (q.trackLst) {
178
+ q2.trackLst = q.trackLst;
179
+ }
180
+ if (q.chat) {
181
+ q2.chat = {};
182
+ }
180
183
  if (q.NIdata && serverconfig.features.showBrainImaging) {
181
184
  q2.NIdata = {};
182
185
  for (const k in q.NIdata) {
@@ -56,6 +56,10 @@ async function trigger_getViolinPlotData(q, ds) {
56
56
  },
57
57
  ds
58
58
  );
59
+ const samples = Object.values(data.samples);
60
+ let values = samples.map((s) => s?.[q.tw.$id]?.value).filter((v) => typeof v === "number");
61
+ if (q.unit == "log") values = values.filter((v) => v > 0);
62
+ const descrStats = summaryStats(values).values;
59
63
  const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
60
64
  if (data.error) throw data.error;
61
65
  if (q.overlayTw && data.refs.byTermId[q.overlayTw.$id]) {
@@ -71,6 +75,7 @@ async function trigger_getViolinPlotData(q, ds) {
71
75
  const result = setResponse(valuesObject, data, q);
72
76
  if (q.overlayTw) await getWilcoxonData(result);
73
77
  await createCanvasImg(q, result, ds);
78
+ result["descrStats"] = descrStats;
74
79
  return result;
75
80
  }
76
81
  async function getWilcoxonData(result) {
@@ -40,7 +40,7 @@ function init({ genomes }) {
40
40
  const setCookie = promisify(cookieJar.setCookie.bind(cookieJar));
41
41
  const getCookieString = promisify(cookieJar.getCookieString.bind(cookieJar));
42
42
  const wsiImagePath = await getWSImagePath(ds, wSImagesRequest);
43
- const session = await getSessionId(ds, cookieJar, getCookieString, setCookie, wsiImagePath, aiProjectId);
43
+ const session = await getSessionId(ds, cookieJar, getCookieString, setCookie, wsiImagePath);
44
44
  const getWsiImageResponse = await getWsiImageDimensions(
45
45
  session.imageSessionId,
46
46
  getCookieString,
@@ -74,7 +74,7 @@ async function getWSImagePath(ds, wSImagesRequest) {
74
74
  return path.join(`${mount}/${ds.queries.WSImages.aiToolImageFolder}/`, wSImagesRequest.wsimage);
75
75
  }
76
76
  }
77
- async function getSessionId(ds, cookieJar, getCookieString, setCookie, wsimage, aiProjectId) {
77
+ async function getSessionId(ds, cookieJar, getCookieString, setCookie, wsimage) {
78
78
  const sessionManager = SessionManager.getInstance();
79
79
  const invalidateResult = await sessionManager.syncAndInvalidateSessions(wsimage);
80
80
  if (!invalidateResult) throw new Error("Session invalidation failed");
@@ -154,44 +154,6 @@ async function getSessionId(ds, cookieJar, getCookieString, setCookie, wsimage,
154
154
  }
155
155
  }
156
156
  const sessionData = await sessionManager.setSession(wsimage, sessionId, tileServer, overlays);
157
- if (ds.queries.WSImages.getWSIPredictionPatches) {
158
- const predictionPatches = await ds.queries.WSImages.getWSIPredictionPatches(aiProjectId, wsimage);
159
- if (!predictionPatches) throw new Error("No prediction files found");
160
- const mount = serverconfig.features?.tileserver?.mount;
161
- if (!mount) throw new Error("No mount available for TileServer");
162
- if (predictionPatches.length > 0) {
163
- for (const predictionPatch of predictionPatches) {
164
- const predictionFilePath = path.join(`${mount}/${ds.queries.WSImages.aiToolImageFolder}/`, predictionPatch);
165
- const predictionsData = qs.stringify({
166
- overlay_path: predictionFilePath
167
- });
168
- await ky.put(`${tileServer.url}/tileserver/overlay`, {
169
- body: predictionsData,
170
- timeout: 5e4,
171
- headers: {
172
- "Content-Type": "application/x-www-form-urlencoded",
173
- Cookie: `session_id=${sessionId}`
174
- },
175
- hooks: getHooks(cookieJar, getCookieString, setCookie)
176
- });
177
- }
178
- const cmapData = qs.stringify({
179
- cmap: JSON.stringify({
180
- keys: ["prediction", "annotation"],
181
- values: [ds.queries.WSImages.predictionColor, ds.queries.WSImages.annotationsColor]
182
- })
183
- });
184
- await ky.put(`${tileServer.url}/tileserver/cmap`, {
185
- body: cmapData,
186
- timeout: 5e4,
187
- headers: {
188
- "Content-Type": "application/x-www-form-urlencoded",
189
- Cookie: `session_id=${sessionId}`
190
- },
191
- hooks: getHooks(cookieJar, getCookieString, setCookie)
192
- });
193
- }
194
- }
195
157
  return sessionData;
196
158
  }
197
159
  async function getWsiImageDimensions(sessionId, getCookieString, wsimage) {