@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 +5 -7
- package/routes/aiProjectSelectedWSImages.js +0 -3
- package/routes/termdb.boxplot.js +20 -30
- package/routes/termdb.chat.js +71 -0
- package/routes/termdb.cohort.summary.js +6 -1
- package/routes/termdb.config.js +9 -6
- package/routes/termdb.violin.js +5 -0
- package/routes/wsimages.js +2 -40
- package/src/app.js +262 -240
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
64
|
+
"@sjcrh/proteinpaint-python": "2.146.0",
|
|
65
65
|
"@sjcrh/proteinpaint-r": "2.145.1-0",
|
|
66
|
-
"@sjcrh/proteinpaint-rust": "2.
|
|
67
|
-
"@sjcrh/proteinpaint-shared": "2.145.
|
|
68
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
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",
|
package/routes/termdb.boxplot.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
51
|
-
const sortedValues =
|
|
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
|
|
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=${
|
|
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=${
|
|
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
|
-
|
|
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 });
|
package/routes/termdb.config.js
CHANGED
|
@@ -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) {
|
package/routes/termdb.violin.js
CHANGED
|
@@ -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) {
|
package/routes/wsimages.js
CHANGED
|
@@ -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
|
|
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
|
|
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) {
|