@datagrok/proteomics 1.0.0 → 1.2.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/CHANGELOG.md +52 -3
- package/CLAUDE.md +178 -0
- package/README.md +195 -3
- package/detectors.js +152 -9
- package/dist/package-test.js +1 -1744
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -146
- package/dist/package.js.map +1 -1
- package/docs/personas-and-capabilities.md +165 -0
- package/files/demo/README.md +264 -0
- package/files/demo/cptac-spike-in.txt +1571 -0
- package/files/demo/enrichment-demo.csv +120 -0
- package/files/demo/fragpipe-smoke-test.tsv +11 -0
- package/files/demo/proteinGroups.txt +28 -0
- package/files/demo/spectronaut-hye-candidates.tsv +94 -0
- package/files/demo/spectronaut-hye-demo.tsv +8761 -0
- package/files/demo/spectronaut-hye-mix.tsv +8761 -0
- package/files/demo/spectronaut-hye-precursor-golden.json +938 -0
- package/files/demo/spectronaut-hye-precursor-golden.tsv +235 -0
- package/files/demo/spectronaut-hye-precursor.tsv +493 -0
- package/images/enrichment-crosslink.png +0 -0
- package/images/enrichment-term-selected.png +0 -0
- package/images/hero.png +0 -0
- package/images/pipeline.svg +80 -0
- package/package.json +87 -62
- package/scripts/deqms_de.R +60 -0
- package/scripts/limma_de.R +42 -0
- package/scripts/vsn_normalize.R +19 -0
- package/src/analysis/differential-expression.ts +450 -0
- package/src/analysis/enrichment-export.ts +101 -0
- package/src/analysis/enrichment.ts +602 -0
- package/src/analysis/experiment-setup.ts +199 -0
- package/src/analysis/imputation.ts +407 -0
- package/src/analysis/log2-scale.ts +139 -0
- package/src/analysis/normalization.ts +255 -0
- package/src/analysis/pca.ts +254 -0
- package/src/analysis/spc-storage.ts +515 -0
- package/src/analysis/spc.ts +544 -0
- package/src/analysis/subcellular-location.ts +431 -0
- package/src/demo/enrichment-demo.ts +94 -0
- package/src/demo/proteomics-demo.ts +123 -0
- package/src/global.d.ts +15 -0
- package/src/menu.ts +133 -0
- package/src/package-api.ts +136 -14
- package/src/package-test.ts +45 -20
- package/src/package.g.ts +161 -0
- package/src/package.ts +1029 -17
- package/src/panels/protein-focus.ts +63 -0
- package/src/panels/published-analysis-panel.ts +151 -0
- package/src/panels/uniprot-panel.ts +349 -0
- package/src/parsers/fragpipe-parser.ts +200 -0
- package/src/parsers/generic-parser.ts +197 -0
- package/src/parsers/maxquant-parser.ts +162 -0
- package/src/parsers/shared-utils.ts +163 -0
- package/src/parsers/spectronaut-candidates-parser.ts +307 -0
- package/src/parsers/spectronaut-parser.ts +604 -0
- package/src/publishing/assert-published-shape.ts +260 -0
- package/src/publishing/post-open-recovery.ts +104 -0
- package/src/publishing/publish-project.ts +515 -0
- package/src/publishing/publish-settings.ts +59 -0
- package/src/publishing/publish-state.ts +316 -0
- package/src/publishing/share-dialog.ts +171 -0
- package/src/publishing/trim-dataframe.ts +247 -0
- package/src/tests/analysis.ts +658 -0
- package/src/tests/enrichment-export.ts +61 -0
- package/src/tests/enrichment-visualization.ts +340 -0
- package/src/tests/enrichment.ts +224 -0
- package/src/tests/fragpipe-e2e.ts +74 -0
- package/src/tests/fragpipe-parser.ts +147 -0
- package/src/tests/gene-label-resolver.ts +387 -0
- package/src/tests/generic-parser.ts +152 -0
- package/src/tests/group-mean-correlation.ts +139 -0
- package/src/tests/log2-scale.ts +93 -0
- package/src/tests/organisms.ts +56 -0
- package/src/tests/parsers.ts +182 -0
- package/src/tests/publish-roundtrip.ts +584 -0
- package/src/tests/publish-spike.ts +377 -0
- package/src/tests/qc-dashboard.ts +210 -0
- package/src/tests/smart-pathway-filter.ts +193 -0
- package/src/tests/spc-formula-lines-spike.ts +129 -0
- package/src/tests/spc.ts +640 -0
- package/src/tests/spectronaut-candidates-e2e.ts +140 -0
- package/src/tests/spectronaut-candidates-parser.ts +398 -0
- package/src/tests/spectronaut-parser.ts +668 -0
- package/src/tests/subcellular-location.ts +361 -0
- package/src/tests/uniprot-panel.ts +202 -0
- package/src/tests/volcano.ts +603 -0
- package/src/utils/column-detection.ts +28 -0
- package/src/utils/gene-label-resolver.ts +443 -0
- package/src/utils/organisms.ts +82 -0
- package/src/utils/proteomics-types.ts +30 -0
- package/src/viewers/enrichment-viewers.ts +274 -0
- package/src/viewers/group-mean-correlation.ts +218 -0
- package/src/viewers/heatmap.ts +168 -0
- package/src/viewers/pca-plot.ts +169 -0
- package/src/viewers/qc-computations.ts +266 -0
- package/src/viewers/qc-dashboard.ts +176 -0
- package/src/viewers/spc-dashboard.ts +755 -0
- package/src/viewers/volcano.ts +690 -0
- package/test-console-output-1.log +2055 -0
- package/test-record-1.mp4 +0 -0
- package/tools/derive-precursor-golden-sidecar.mjs +81 -0
- package/tools/generate-enrichment-fixture.sh +160 -0
- package/tools/generate-spectronaut-candidates-fixture.mjs +212 -0
- package/tools/generate-spectronaut-precursor-fixture.mjs +128 -0
- package/tools/spectronaut-aggregate.sh +46 -0
- package/tools/spectronaut-aggregate.sql +80 -0
- package/tsconfig.json +18 -71
- package/webpack.config.js +86 -45
- package/.eslintignore +0 -1
- package/.eslintrc.json +0 -56
- package/LICENSE +0 -674
- package/package.png +0 -0
- package/scripts/number_antibody.py +0 -190
- package/scripts/number_antibody_abnumber.py +0 -177
- package/scripts/number_antibody_anarci.py +0 -200
|
Binary file
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Derives files/demo/spectronaut-hye-precursor-golden.json from the committed
|
|
4
|
+
* duckdb golden files/demo/spectronaut-hye-precursor-golden.tsv.
|
|
5
|
+
*
|
|
6
|
+
* This script does NO aggregation — it is a pure, lossless transcription of the
|
|
7
|
+
* already-committed duckdb output into a JSON map the in-browser `grok test`
|
|
8
|
+
* runner can read with certainty when committed-file reads are unavailable in
|
|
9
|
+
* the test runner. Because it never re-computes anything, the equivalence test
|
|
10
|
+
* stays pinned to REAL duckdb output (the D-04 oracle), not to a hand-derived
|
|
11
|
+
* approximation. Re-running it against an unchanged golden produces a
|
|
12
|
+
* byte-identical JSON.
|
|
13
|
+
*
|
|
14
|
+
* Regen chain (run in order, or the artifacts silently drift):
|
|
15
|
+
* 1. node tools/generate-spectronaut-precursor-fixture.mjs
|
|
16
|
+
* 2. tools/spectronaut-aggregate.sh files/demo/spectronaut-hye-precursor.tsv \
|
|
17
|
+
* files/demo/spectronaut-hye-precursor-golden.tsv
|
|
18
|
+
* 3. node tools/derive-precursor-golden-sidecar.mjs (this script)
|
|
19
|
+
*
|
|
20
|
+
* Run: node tools/derive-precursor-golden-sidecar.mjs
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import {fileURLToPath} from 'node:url';
|
|
26
|
+
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = path.dirname(__filename);
|
|
29
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
30
|
+
const GOLDEN = path.join(PKG_ROOT, 'files/demo/spectronaut-hye-precursor-golden.tsv');
|
|
31
|
+
const OUTPUT = path.join(PKG_ROOT, 'files/demo/spectronaut-hye-precursor-golden.json');
|
|
32
|
+
|
|
33
|
+
// Quantity column preference order — same as the in-package Spectronaut parser
|
|
34
|
+
// (src/parsers/spectronaut-parser.ts QUANTITY_COLUMNS).
|
|
35
|
+
const QUANTITY_COLUMNS = ['PG.IBAQ', 'PG.Quantity'];
|
|
36
|
+
|
|
37
|
+
const text = fs.readFileSync(GOLDEN, 'utf8');
|
|
38
|
+
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
|
39
|
+
if (lines.length < 2)
|
|
40
|
+
throw new Error(`Golden ${GOLDEN} has no data rows — run the aggregate wrapper first.`);
|
|
41
|
+
|
|
42
|
+
const headers = lines[0].split('\t');
|
|
43
|
+
const idx = Object.fromEntries(headers.map((h, i) => [h, i]));
|
|
44
|
+
|
|
45
|
+
const protIdx = idx['PG.ProteinGroups'];
|
|
46
|
+
const condIdx = idx['R.Condition'];
|
|
47
|
+
const replIdx = idx['R.Replicate'];
|
|
48
|
+
const qvalIdx = idx['EG.Qvalue'];
|
|
49
|
+
const quantCol = QUANTITY_COLUMNS.find((c) => idx[c] !== undefined);
|
|
50
|
+
|
|
51
|
+
if (protIdx === undefined || condIdx === undefined || replIdx === undefined ||
|
|
52
|
+
qvalIdx === undefined || quantCol === undefined) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Golden ${GOLDEN} is missing an expected column. ` +
|
|
55
|
+
`Need PG.ProteinGroups / R.Condition / R.Replicate / EG.Qvalue / ` +
|
|
56
|
+
`${QUANTITY_COLUMNS.join('|')}. Got: ${headers.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
const quantIdx = idx[quantCol];
|
|
59
|
+
|
|
60
|
+
const out = {};
|
|
61
|
+
for (let i = 1; i < lines.length; i++) {
|
|
62
|
+
const f = lines[i].split('\t');
|
|
63
|
+
const protein = f[protIdx];
|
|
64
|
+
const condition = f[condIdx];
|
|
65
|
+
const replicate = f[replIdx];
|
|
66
|
+
// Numbers exactly as duckdb wrote them. Empty q-value (TRY_CAST→NULL min,
|
|
67
|
+
// e.g. the non-numeric / empty-string-q-value proteins) → Number('') is 0;
|
|
68
|
+
// keep the verbatim transcription rather than inventing a sentinel.
|
|
69
|
+
const key = `${protein}${condition}_${replicate}`;
|
|
70
|
+
out[key] = {
|
|
71
|
+
quantity: Number(f[quantIdx]),
|
|
72
|
+
qvalue: Number(f[qvalIdx] ?? ''),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(OUTPUT, JSON.stringify(out, null, 2) + '\n');
|
|
77
|
+
const keys = Object.keys(out);
|
|
78
|
+
console.log(`Wrote ${OUTPUT}`);
|
|
79
|
+
console.log(` ${keys.length} (protein × condition × replicate) entries ` +
|
|
80
|
+
`transcribed verbatim from ${path.basename(GOLDEN)} ` +
|
|
81
|
+
`(quantity column: ${quantCol}).`);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Engineered human Treatment-vs-Control DE result for the enrichment demo.
|
|
3
|
+
# UP = cell cycle / mitosis, DOWN = oxidative phosphorylation, plus a diverse
|
|
4
|
+
# non-significant background. Gene symbols are real HGNC; accessions real UniProt.
|
|
5
|
+
# Fold-changes and p-values are spread with a DETERMINISTIC sin-hash jitter so
|
|
6
|
+
# the volcano looks organic (no banding) yet regenerates identically anywhere.
|
|
7
|
+
out=files/demo/enrichment-demo.csv
|
|
8
|
+
echo "Protein ID,Gene,log2FC,p-value,adj.p-value,significant" > "$out"
|
|
9
|
+
|
|
10
|
+
UP="CDK1 P06493
|
|
11
|
+
CCNB1 P14635
|
|
12
|
+
CCNB2 O95067
|
|
13
|
+
CDC20 Q12834
|
|
14
|
+
BUB1 O43683
|
|
15
|
+
BUB1B O60566
|
|
16
|
+
AURKA O14965
|
|
17
|
+
AURKB Q96GD4
|
|
18
|
+
PLK1 P53350
|
|
19
|
+
CCNA2 P20248
|
|
20
|
+
CDK2 P24941
|
|
21
|
+
MAD2L1 Q13257
|
|
22
|
+
TOP2A P11388
|
|
23
|
+
KIF11 P52732
|
|
24
|
+
CENPA P49450
|
|
25
|
+
CENPE Q02224
|
|
26
|
+
NDC80 O14777
|
|
27
|
+
CDC6 Q99741
|
|
28
|
+
CDC45 O75419
|
|
29
|
+
MCM2 P49736
|
|
30
|
+
MCM3 P25205
|
|
31
|
+
MCM4 P33991
|
|
32
|
+
MCM5 P33992
|
|
33
|
+
MCM6 Q14566
|
|
34
|
+
MCM7 P33993
|
|
35
|
+
CDC25C P30307
|
|
36
|
+
CDK4 P11802
|
|
37
|
+
CCNE1 P24864
|
|
38
|
+
CHEK1 O14757
|
|
39
|
+
WEE1 P30291"
|
|
40
|
+
|
|
41
|
+
DOWN="NDUFA9 Q16795
|
|
42
|
+
NDUFS1 P28331
|
|
43
|
+
NDUFS2 O75306
|
|
44
|
+
NDUFB10 O96000
|
|
45
|
+
NDUFV1 P49821
|
|
46
|
+
NDUFA4 O00483
|
|
47
|
+
NDUFB8 O95169
|
|
48
|
+
SDHA P31040
|
|
49
|
+
SDHB P21912
|
|
50
|
+
UQCRC1 P31930
|
|
51
|
+
UQCRC2 P22695
|
|
52
|
+
UQCRFS1 P47985
|
|
53
|
+
CYC1 P08574
|
|
54
|
+
COX4I1 P13073
|
|
55
|
+
COX5A P20674
|
|
56
|
+
COX5B P10606
|
|
57
|
+
COX6C P09669
|
|
58
|
+
COX7A2 P14406
|
|
59
|
+
COX6B1 P14854
|
|
60
|
+
ATP5F1A P25705
|
|
61
|
+
ATP5F1B P06576
|
|
62
|
+
ATP5F1C P36542
|
|
63
|
+
ATP5PO P48047
|
|
64
|
+
ATP5PB P24539
|
|
65
|
+
ATP5MC1 P05496"
|
|
66
|
+
|
|
67
|
+
BG="GAPDH P04406
|
|
68
|
+
ACTB P60709
|
|
69
|
+
TUBB P07437
|
|
70
|
+
ALB P02768
|
|
71
|
+
ENO1 P06733
|
|
72
|
+
PKM P14618
|
|
73
|
+
LDHA P00338
|
|
74
|
+
PGK1 P00558
|
|
75
|
+
ALDOA P04075
|
|
76
|
+
TPI1 P60174
|
|
77
|
+
GPI P06744
|
|
78
|
+
PGAM1 P18669
|
|
79
|
+
HSPA8 P11142
|
|
80
|
+
HSP90AA1 P07900
|
|
81
|
+
HSPA5 P11021
|
|
82
|
+
HSPD1 P10809
|
|
83
|
+
CALR P27797
|
|
84
|
+
CANX P27824
|
|
85
|
+
PDIA3 P30101
|
|
86
|
+
P4HB P07237
|
|
87
|
+
VIM P08670
|
|
88
|
+
FLNA P21333
|
|
89
|
+
ACTN1 P12814
|
|
90
|
+
TLN1 Q9Y490
|
|
91
|
+
CFL1 P23528
|
|
92
|
+
PFN1 P07737
|
|
93
|
+
GSN P06396
|
|
94
|
+
CAP1 Q01518
|
|
95
|
+
ANXA1 P04083
|
|
96
|
+
ANXA2 P07355
|
|
97
|
+
ANXA5 P08758
|
|
98
|
+
S100A4 P26447
|
|
99
|
+
YWHAZ P63104
|
|
100
|
+
YWHAE P62258
|
|
101
|
+
YWHAB P31946
|
|
102
|
+
PPIA P62937
|
|
103
|
+
PPIB P23284
|
|
104
|
+
SOD1 P00441
|
|
105
|
+
SOD2 P04179
|
|
106
|
+
CAT P04040
|
|
107
|
+
PRDX1 Q06830
|
|
108
|
+
PRDX2 P32119
|
|
109
|
+
GSTP1 P09211
|
|
110
|
+
TXN P10599
|
|
111
|
+
FTL P02792
|
|
112
|
+
FTH1 P02794
|
|
113
|
+
TF P02787
|
|
114
|
+
CTSD P07339
|
|
115
|
+
CTSB P07858
|
|
116
|
+
LGALS1 P09382
|
|
117
|
+
LGALS3 P17931
|
|
118
|
+
CD44 P16070
|
|
119
|
+
ITGB1 P05556
|
|
120
|
+
EEF1A1 P68104
|
|
121
|
+
EEF2 P13639
|
|
122
|
+
RPS3 P23396
|
|
123
|
+
RPL4 P36578
|
|
124
|
+
RPLP0 P05388
|
|
125
|
+
NPM1 P06748
|
|
126
|
+
NCL P19338
|
|
127
|
+
HNRNPK P61978
|
|
128
|
+
SRSF1 Q07955
|
|
129
|
+
G3BP1 Q13283
|
|
130
|
+
DDX5 P17844"
|
|
131
|
+
|
|
132
|
+
# deterministic pseudo-random in [0,1) from a seed integer and a multiplier
|
|
133
|
+
emit() { # $1=list $2=sign(+1/-1) for sig sets, 0=background
|
|
134
|
+
local sign="$2"; local i=0
|
|
135
|
+
while read -r gene acc; do
|
|
136
|
+
[ -z "$gene" ] && continue
|
|
137
|
+
awk -v i="$i" -v gene="$gene" -v acc="$acc" -v sign="$sign" 'BEGIN{
|
|
138
|
+
h1=sin((i+1)*12.9898)*43758.5453; j1=h1-int(h1); if(j1<0)j1+=1
|
|
139
|
+
h2=sin((i+1)*78.2330)*24634.6345; j2=h2-int(h2); if(j2<0)j2+=1
|
|
140
|
+
if (sign!=0) {
|
|
141
|
+
fc=sign*(1.2 + j1*2.3) # |fc| 1.2..3.5
|
|
142
|
+
e=2.5 + j2*4.5 # -log10(p) 2.5..7.0
|
|
143
|
+
p=exp(-e*log(10)); adj=2*p
|
|
144
|
+
printf "%s,%s,%.3f,%.2e,%.2e,true\n", acc, gene, fc, p, adj
|
|
145
|
+
} else {
|
|
146
|
+
fc=-1.3 + j1*2.6 # spread -1.3..1.3
|
|
147
|
+
p=0.06 + j2*0.90 # 0.06..0.96 (never significant)
|
|
148
|
+
adj=p*1.05; if(adj>0.99)adj=0.99
|
|
149
|
+
printf "%s,%s,%.3f,%.3f,%.3f,false\n", acc, gene, fc, p, adj
|
|
150
|
+
}
|
|
151
|
+
}' >> "$out"
|
|
152
|
+
i=$((i+1))
|
|
153
|
+
done <<< "$1"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
emit "$UP" 1
|
|
157
|
+
emit "$DOWN" -1
|
|
158
|
+
emit "$BG" 0
|
|
159
|
+
|
|
160
|
+
echo "rows: $(($(wc -l < "$out") - 1)) (sig=$(grep -c ',true$' "$out"), ns=$(grep -c ',false$' "$out"))"
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Generates files/demo/spectronaut-hye-candidates.tsv from the already-shipped
|
|
4
|
+
* files/demo/spectronaut-hye-mix.tsv (HYE three-species mix, SpectroPipeR origin).
|
|
5
|
+
*
|
|
6
|
+
* Source data is real Spectronaut long-format PG quantification. The Candidates
|
|
7
|
+
* report it ships alongside is DERIVED — we run our own Welch's t-test with BH
|
|
8
|
+
* FDR correction in this script and emit the result formatted as a Spectronaut
|
|
9
|
+
* Candidates report. This is not a verbatim Spectronaut export; it exists to
|
|
10
|
+
* exercise the Candidates import path with values that line up with the source
|
|
11
|
+
* PG file so the two demo fixtures tell the same story.
|
|
12
|
+
*
|
|
13
|
+
* Run: node tools/generate-spectronaut-candidates-fixture.mjs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import {fileURLToPath} from 'node:url';
|
|
19
|
+
import jStat from 'jstat';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
24
|
+
const SOURCE = path.join(PKG_ROOT, 'files/demo/spectronaut-hye-mix.tsv');
|
|
25
|
+
const OUTPUT = path.join(PKG_ROOT, 'files/demo/spectronaut-hye-candidates.tsv');
|
|
26
|
+
|
|
27
|
+
// Numerator (group1) and denominator (group2) per Spectronaut convention.
|
|
28
|
+
// AVG Log2 Ratio = log2(numerator / denominator). The HYE mix is symmetric;
|
|
29
|
+
// labelling B as the numerator just keeps the convention legible.
|
|
30
|
+
const NUMERATOR = 'HYE mix B';
|
|
31
|
+
const DENOMINATOR = 'HYE mix A';
|
|
32
|
+
|
|
33
|
+
/** Read TSV file into header + rows. */
|
|
34
|
+
function readTsv(filePath) {
|
|
35
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
36
|
+
const lines = text.split(/\r?\n/);
|
|
37
|
+
const headers = lines[0].split('\t');
|
|
38
|
+
const idx = Object.fromEntries(headers.map((h, i) => [h, i]));
|
|
39
|
+
const rows = [];
|
|
40
|
+
for (let i = 1; i < lines.length; i++) {
|
|
41
|
+
if (!lines[i]) continue;
|
|
42
|
+
const fields = lines[i].split('\t');
|
|
43
|
+
rows.push(fields);
|
|
44
|
+
}
|
|
45
|
+
return {headers, idx, rows};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Pivot long PG report: protein -> {sampleKey -> IBAQ}. First-encountered
|
|
49
|
+
* non-empty IBAQ wins per protein × sample (constant per protein × sample in
|
|
50
|
+
* SpectroPipeR's report; mirrors the in-package Spectronaut parser). */
|
|
51
|
+
function pivotProteinIbaq(headers, idx, rows) {
|
|
52
|
+
const protein = {};
|
|
53
|
+
for (const r of rows) {
|
|
54
|
+
const pg = r[idx['PG.ProteinGroups']];
|
|
55
|
+
const cond = r[idx['R.Condition']];
|
|
56
|
+
const repl = r[idx['R.Replicate']];
|
|
57
|
+
const ibaqRaw = r[idx['PG.IBAQ']];
|
|
58
|
+
const org = r[idx['PG.Organisms']];
|
|
59
|
+
if (!pg || !cond || !repl) continue;
|
|
60
|
+
const ibaq = Number(ibaqRaw);
|
|
61
|
+
if (!Number.isFinite(ibaq) || ibaq <= 0) continue;
|
|
62
|
+
const sampleKey = `${cond}|${repl}`;
|
|
63
|
+
if (!protein[pg]) protein[pg] = {samples: {}, organism: org || ''};
|
|
64
|
+
if (protein[pg].samples[sampleKey] === undefined)
|
|
65
|
+
protein[pg].samples[sampleKey] = ibaq;
|
|
66
|
+
if (!protein[pg].organism && org) protein[pg].organism = org;
|
|
67
|
+
}
|
|
68
|
+
return protein;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Welch's two-sample t-test using jStat. Returns null when either sample has
|
|
72
|
+
* fewer than two finite values. */
|
|
73
|
+
function welchTTest(a, b) {
|
|
74
|
+
const af = a.filter(Number.isFinite);
|
|
75
|
+
const bf = b.filter(Number.isFinite);
|
|
76
|
+
if (af.length < 2 || bf.length < 2) return null;
|
|
77
|
+
const meanA = jStat.mean(af);
|
|
78
|
+
const meanB = jStat.mean(bf);
|
|
79
|
+
const varA = jStat.variance(af, true);
|
|
80
|
+
const varB = jStat.variance(bf, true);
|
|
81
|
+
const seA = varA / af.length;
|
|
82
|
+
const seB = varB / bf.length;
|
|
83
|
+
const se = Math.sqrt(seA + seB);
|
|
84
|
+
if (se === 0) return {t: 0, df: af.length + bf.length - 2, p: 1, meanA, meanB};
|
|
85
|
+
const t = (meanB - meanA) / se;
|
|
86
|
+
// Welch–Satterthwaite degrees of freedom
|
|
87
|
+
const df = Math.pow(seA + seB, 2) /
|
|
88
|
+
(Math.pow(seA, 2) / (af.length - 1) + Math.pow(seB, 2) / (bf.length - 1));
|
|
89
|
+
const pTwoSided = 2 * (1 - jStat.studentt.cdf(Math.abs(t), df));
|
|
90
|
+
return {t, df, p: pTwoSided, meanA, meanB};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Benjamini–Hochberg FDR correction. Returns array of adjusted p-values aligned
|
|
94
|
+
* to input order; entries left as null where the input is null. */
|
|
95
|
+
function bhAdjust(pvals) {
|
|
96
|
+
const indexed = pvals.map((p, i) => ({p, i})).filter((x) => x.p !== null);
|
|
97
|
+
indexed.sort((a, b) => a.p - b.p);
|
|
98
|
+
const n = indexed.length;
|
|
99
|
+
const adj = new Array(pvals.length).fill(null);
|
|
100
|
+
let cumMin = 1;
|
|
101
|
+
for (let rank = n; rank >= 1; rank--) {
|
|
102
|
+
const e = indexed[rank - 1];
|
|
103
|
+
const q = e.p * n / rank;
|
|
104
|
+
cumMin = Math.min(cumMin, q);
|
|
105
|
+
adj[e.i] = Math.min(cumMin, 1);
|
|
106
|
+
}
|
|
107
|
+
return adj;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function main() {
|
|
111
|
+
const {headers, idx, rows} = readTsv(SOURCE);
|
|
112
|
+
console.log(`Read ${rows.length} long-format rows, ${headers.length} columns.`);
|
|
113
|
+
|
|
114
|
+
const protein = pivotProteinIbaq(headers, idx, rows);
|
|
115
|
+
const proteins = Object.keys(protein).sort();
|
|
116
|
+
console.log(`Pivoted to ${proteins.length} protein groups.`);
|
|
117
|
+
|
|
118
|
+
// Sample keys per group
|
|
119
|
+
const sampleKey = (c, r) => `${c}|${r}`;
|
|
120
|
+
const groupSamples = (cond) => [1, 2, 3, 4].map((r) => sampleKey(cond, String(r)));
|
|
121
|
+
const numKeys = groupSamples(NUMERATOR);
|
|
122
|
+
const denKeys = groupSamples(DENOMINATOR);
|
|
123
|
+
|
|
124
|
+
// Per-protein log2FC + raw p-value
|
|
125
|
+
const proteinStats = proteins.map((pg) => {
|
|
126
|
+
const samples = protein[pg].samples;
|
|
127
|
+
const num = numKeys.map((k) => samples[k]).filter(Number.isFinite);
|
|
128
|
+
const den = denKeys.map((k) => samples[k]).filter(Number.isFinite);
|
|
129
|
+
const numLog2 = num.map((x) => Math.log2(x));
|
|
130
|
+
const denLog2 = den.map((x) => Math.log2(x));
|
|
131
|
+
const t = welchTTest(denLog2, numLog2); // a=denominator, b=numerator
|
|
132
|
+
if (!t) {
|
|
133
|
+
return {
|
|
134
|
+
pg,
|
|
135
|
+
organism: protein[pg].organism,
|
|
136
|
+
log2fc: null,
|
|
137
|
+
p: null,
|
|
138
|
+
meanNum: num.length > 0 ? jStat.mean(num) : null,
|
|
139
|
+
meanDen: den.length > 0 ? jStat.mean(den) : null,
|
|
140
|
+
nRatios: Math.min(num.length, den.length),
|
|
141
|
+
valid: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
pg,
|
|
146
|
+
organism: protein[pg].organism,
|
|
147
|
+
log2fc: t.meanB - t.meanA, // log2(num) - log2(den)
|
|
148
|
+
p: t.p,
|
|
149
|
+
meanNum: jStat.mean(num),
|
|
150
|
+
meanDen: jStat.mean(den),
|
|
151
|
+
nRatios: Math.min(num.length, den.length),
|
|
152
|
+
valid: true,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const adj = bhAdjust(proteinStats.map((s) => s.p));
|
|
157
|
+
|
|
158
|
+
// Write Candidates TSV
|
|
159
|
+
const outHeaders = [
|
|
160
|
+
'Valid', 'Comparison (group1/group2)',
|
|
161
|
+
'Condition Numerator', 'Condition Denominator',
|
|
162
|
+
'# of Ratios', 'Group',
|
|
163
|
+
'AVG Group Quantity Numerator', 'AVG Group Quantity Denominator',
|
|
164
|
+
'AVG Log2 Ratio', 'Absolute AVG Log2 Ratio',
|
|
165
|
+
'% Change', 'Ratio',
|
|
166
|
+
'Pvalue', 'Qvalue',
|
|
167
|
+
'ProteinGroups', 'Genes', 'UniProtIds', 'Organisms',
|
|
168
|
+
];
|
|
169
|
+
const comparison = `${NUMERATOR} / ${DENOMINATOR}`;
|
|
170
|
+
|
|
171
|
+
const lines = [outHeaders.join('\t')];
|
|
172
|
+
let nSig = 0, nUp = 0, nDown = 0;
|
|
173
|
+
for (let i = 0; i < proteinStats.length; i++) {
|
|
174
|
+
const s = proteinStats[i];
|
|
175
|
+
const q = adj[i];
|
|
176
|
+
const ratio = (s.meanNum != null && s.meanDen != null && s.meanDen > 0)
|
|
177
|
+
? (s.meanNum / s.meanDen) : null;
|
|
178
|
+
const pctChange = ratio != null ? (ratio - 1) * 100 : null;
|
|
179
|
+
const row = [
|
|
180
|
+
s.valid ? 'True' : 'False',
|
|
181
|
+
comparison,
|
|
182
|
+
NUMERATOR,
|
|
183
|
+
DENOMINATOR,
|
|
184
|
+
String(s.nRatios),
|
|
185
|
+
s.pg,
|
|
186
|
+
s.meanNum != null ? s.meanNum.toFixed(4) : '',
|
|
187
|
+
s.meanDen != null ? s.meanDen.toFixed(4) : '',
|
|
188
|
+
s.log2fc != null ? s.log2fc.toFixed(6) : '',
|
|
189
|
+
s.log2fc != null ? Math.abs(s.log2fc).toFixed(6) : '',
|
|
190
|
+
pctChange != null ? pctChange.toFixed(4) : '',
|
|
191
|
+
ratio != null ? ratio.toFixed(6) : '',
|
|
192
|
+
s.p != null ? s.p.toExponential(6) : '',
|
|
193
|
+
q != null ? q.toExponential(6) : '',
|
|
194
|
+
s.pg, // ProteinGroups (same as Group here)
|
|
195
|
+
'', // Genes (HYE PG report has no symbols)
|
|
196
|
+
s.pg, // UniProtIds (PG.ProteinGroups in this dataset are UniProt accs)
|
|
197
|
+
s.organism,
|
|
198
|
+
];
|
|
199
|
+
lines.push(row.join('\t'));
|
|
200
|
+
|
|
201
|
+
if (s.log2fc != null && q != null && Math.abs(s.log2fc) >= 1 && q <= 0.05) {
|
|
202
|
+
nSig++;
|
|
203
|
+
if (s.log2fc > 0) nUp++; else nDown++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fs.writeFileSync(OUTPUT, lines.join('\n') + '\n');
|
|
208
|
+
console.log(`Wrote ${OUTPUT}`);
|
|
209
|
+
console.log(` ${proteinStats.length} rows; ${nSig} significant (${nUp} up, ${nDown} down) at |log2FC|>=1, q<=0.05`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
main();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Generates files/demo/spectronaut-hye-precursor.tsv — a small SYNTHETIC
|
|
4
|
+
* precursor-level Spectronaut long-format report used to exercise the streaming
|
|
5
|
+
* import path (Plan 02) and the duckdb-equivalence golden test (Plan 03).
|
|
6
|
+
*
|
|
7
|
+
* The fixture carries the D-01 precursor signature columns
|
|
8
|
+
* (EG.ModifiedPeptide / FG.Charge / PEP.StrippedSequence) so the header-sniff
|
|
9
|
+
* routes it to the streaming aggregator, uses CondA/CondB (so the committed
|
|
10
|
+
* tools/spectronaut-aggregate.sql DMD↔WT flip is a structural no-op), keeps the
|
|
11
|
+
* quantity value CONSTANT per (protein × condition × replicate) so duckdb max()
|
|
12
|
+
* equals the parser's first-encountered value, and carries NO
|
|
13
|
+
* PG.Genes/PG.ProteinAccessions columns (the real hye-mix demo lacks them and
|
|
14
|
+
* the committed SQL drops the matching any_value SELECT terms for this reason).
|
|
15
|
+
*
|
|
16
|
+
* It deliberately includes one CON__ row, one REV__ row, one protein whose
|
|
17
|
+
* precursors mix a >0.01 sub-threshold q-value with a passing one, one
|
|
18
|
+
* non-numeric-q-value protein, and one empty-q-value protein so every R2 filter
|
|
19
|
+
* branch is exercised by a single fixture.
|
|
20
|
+
*
|
|
21
|
+
* Pure standalone Node (no datagrok-api import); the row shape mirrors the
|
|
22
|
+
* extended makeLongFormatTsv in src/tests/spectronaut-parser.ts.
|
|
23
|
+
*
|
|
24
|
+
* Run: node tools/generate-spectronaut-precursor-fixture.mjs
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import {fileURLToPath} from 'node:url';
|
|
30
|
+
|
|
31
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
32
|
+
const __dirname = path.dirname(__filename);
|
|
33
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
34
|
+
const OUTPUT = path.join(PKG_ROOT, 'files/demo/spectronaut-hye-precursor.tsv');
|
|
35
|
+
|
|
36
|
+
const CONDITIONS = ['CondA', 'CondB'];
|
|
37
|
+
const REPLICATES = [1, 2, 3];
|
|
38
|
+
|
|
39
|
+
// Column order mirrors the extended makeLongFormatTsv header. NO
|
|
40
|
+
// PG.Genes/PG.ProteinAccessions — see doc block.
|
|
41
|
+
const HEADERS = [
|
|
42
|
+
'R.FileName', 'R.Condition', 'R.Replicate', 'PG.ProteinGroups',
|
|
43
|
+
'PG.Organisms', 'PG.Quantity', 'EG.Qvalue', 'PEP.StrippedSequence',
|
|
44
|
+
'EG.ModifiedPeptide', 'FG.Charge', 'FG.Id',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Two distinct precursors per (protein × condition × replicate). Quantity is
|
|
48
|
+
// held constant within each group; only precursor identity varies, so duckdb
|
|
49
|
+
// max() collapses to the same value the parser's first-encountered-wins logic
|
|
50
|
+
// produces.
|
|
51
|
+
const PRECURSORS = [
|
|
52
|
+
{strip: 'PEPTIDER', mod: '_PEPTIDER_', charge: '2', fg: 'FG1'},
|
|
53
|
+
{strip: 'ANOTHERPEP', mod: '_ANOTHERPEP_', charge: '3', fg: 'FG2'},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const ORGANISMS = ['Homo sapiens', 'Saccharomyces cerevisiae', 'Escherichia coli'];
|
|
57
|
+
|
|
58
|
+
const rows = [HEADERS.join('\t')];
|
|
59
|
+
let nDataRows = 0;
|
|
60
|
+
const proteinSet = new Set();
|
|
61
|
+
const sampleSet = new Set();
|
|
62
|
+
|
|
63
|
+
function emit(fileName, cond, rep, id, organism, quantity, qVal) {
|
|
64
|
+
for (const p of PRECURSORS) {
|
|
65
|
+
rows.push([fileName, cond, String(rep), id, organism,
|
|
66
|
+
String(quantity), String(qVal), p.strip, p.mod, p.charge, p.fg].join('\t'));
|
|
67
|
+
nDataRows++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function emitProtein(id, organism, qVal, quantity) {
|
|
72
|
+
proteinSet.add(id);
|
|
73
|
+
for (const cond of CONDITIONS) {
|
|
74
|
+
for (const rep of REPLICATES) {
|
|
75
|
+
const fileName = `run_${cond}_${rep}`;
|
|
76
|
+
sampleSet.add(`${cond}_${rep}`);
|
|
77
|
+
emit(fileName, cond, rep, id, organism, quantity, qVal);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Regular passing proteins: a few dozen, three-species mix, all q<=0.01 ──
|
|
83
|
+
const N_REGULAR = 36;
|
|
84
|
+
let quant = 1000;
|
|
85
|
+
for (let i = 0; i < N_REGULAR; i++) {
|
|
86
|
+
const id = `P${String(i).padStart(5, '0')}`;
|
|
87
|
+
const organism = ORGANISMS[i % ORGANISMS.length];
|
|
88
|
+
emitProtein(id, organism, 0.001, quant++);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Decoy / contaminant rows (dropped by the WHERE CON__/REV__ filter) ──
|
|
92
|
+
emitProtein('CON__P99999', 'Homo sapiens', 0.001, 9000);
|
|
93
|
+
emitProtein('REV__Q88888', 'Homo sapiens', 0.001, 9001);
|
|
94
|
+
|
|
95
|
+
// ── Mixed sub-threshold protein: one passing precursor, one >0.01 dropped ──
|
|
96
|
+
// Emit the rows by hand so the two precursors carry DIFFERENT q-values.
|
|
97
|
+
{
|
|
98
|
+
const id = 'P70001';
|
|
99
|
+
proteinSet.add(id);
|
|
100
|
+
for (const cond of CONDITIONS) {
|
|
101
|
+
for (const rep of REPLICATES) {
|
|
102
|
+
const fileName = `run_${cond}_${rep}`;
|
|
103
|
+
sampleSet.add(`${cond}_${rep}`);
|
|
104
|
+
// passing precursor
|
|
105
|
+
rows.push([fileName, cond, String(rep), id, 'Homo sapiens',
|
|
106
|
+
'7000', '0.002', PRECURSORS[0].strip, PRECURSORS[0].mod,
|
|
107
|
+
PRECURSORS[0].charge, PRECURSORS[0].fg].join('\t'));
|
|
108
|
+
nDataRows++;
|
|
109
|
+
// sub-threshold precursor (q-value 0.05 > 0.01 → dropped by WHERE)
|
|
110
|
+
rows.push([fileName, cond, String(rep), id, 'Homo sapiens',
|
|
111
|
+
'7000', '0.05', PRECURSORS[1].strip, PRECURSORS[1].mod,
|
|
112
|
+
PRECURSORS[1].charge, PRECURSORS[1].fg].join('\t'));
|
|
113
|
+
nDataRows++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Non-numeric q-value protein ('Profiled' → TRY_CAST NULL → passes) ──
|
|
119
|
+
emitProtein('P70002', 'Saccharomyces cerevisiae', 'Profiled', 6500);
|
|
120
|
+
|
|
121
|
+
// ── Empty-string q-value protein (nullstr → NULL → passes) ──
|
|
122
|
+
emitProtein('P70003', 'Escherichia coli', '', 6600);
|
|
123
|
+
|
|
124
|
+
fs.writeFileSync(OUTPUT, rows.join('\n') + '\n');
|
|
125
|
+
console.log(`Wrote ${OUTPUT}`);
|
|
126
|
+
console.log(` ${nDataRows} data rows, ${proteinSet.size} protein groups ` +
|
|
127
|
+
`(incl. CON__/REV__/edge), ${sampleSet.size} samples ` +
|
|
128
|
+
`(${CONDITIONS.length} conditions × ${REPLICATES.length} replicates).`);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Collapse a long-form Spectronaut Report into the protein-group × sample shape
|
|
3
|
+
# the Proteomics package's parseSpectronautText parser consumes.
|
|
4
|
+
#
|
|
5
|
+
# This is the documented MANUAL FALLBACK for a Spectronaut report too large for
|
|
6
|
+
# the in-browser streaming importer: run it locally, then import the small
|
|
7
|
+
# aggregated .tsv it produces. It is also the D-04 equivalence oracle the Wave-0
|
|
8
|
+
# golden test pins to (see files/demo/README.md).
|
|
9
|
+
#
|
|
10
|
+
# Run: tools/spectronaut-aggregate.sh <input.tsv> [output.tsv]
|
|
11
|
+
#
|
|
12
|
+
# Requires the `duckdb` CLI on PATH (v1.3.0 used to derive the committed golden).
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
if [[ $# -lt 1 ]]; then
|
|
17
|
+
echo "Usage: $0 <input.tsv> [output.tsv]" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
IN="$1"
|
|
22
|
+
OUT="${2:-${IN%.tsv}.aggregated.tsv}"
|
|
23
|
+
SQL="$(dirname "$0")/spectronaut-aggregate.sql"
|
|
24
|
+
|
|
25
|
+
if [[ ! -f "$IN" ]]; then
|
|
26
|
+
echo "Input not found: $IN" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "Aggregating: $IN"
|
|
31
|
+
echo "Output: $OUT"
|
|
32
|
+
|
|
33
|
+
# Substitute paths into the SQL template. DuckDB's COPY ... TO doesn't accept
|
|
34
|
+
# variables, so we materialize the SQL first. SQL single-quote escaping: double up
|
|
35
|
+
# any embedded ' in the paths.
|
|
36
|
+
IN_ESC="${IN//\'/\'\'}"
|
|
37
|
+
OUT_ESC="${OUT//\'/\'\'}"
|
|
38
|
+
TMP_SQL="$(mktemp -t spectronaut-aggregate.XXXXXX.sql)"
|
|
39
|
+
trap 'rm -f "$TMP_SQL"' EXIT
|
|
40
|
+
sed -e "s|__IN_PATH__|${IN_ESC}|g" -e "s|__OUT_PATH__|${OUT_ESC}|g" "$SQL" > "$TMP_SQL"
|
|
41
|
+
|
|
42
|
+
duckdb < "$TMP_SQL"
|
|
43
|
+
|
|
44
|
+
echo
|
|
45
|
+
echo "Input size: $(du -h "$IN" | cut -f1)"
|
|
46
|
+
echo "Output size: $(du -h "$OUT" | cut -f1)"
|