@datagrok/proteomics 1.0.1 → 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 -63
- 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
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
import {awaitCheck, category, delay, test} from '@datagrok-libraries/test/src/test';
|
|
4
|
+
import {SEMTYPE} from '../utils/proteomics-types';
|
|
5
|
+
|
|
6
|
+
const SPIKE_TAG_KEYS = [
|
|
7
|
+
'proteomics.source',
|
|
8
|
+
'proteomics.de_method',
|
|
9
|
+
'proteomics.de_complete',
|
|
10
|
+
'proteomics.groups',
|
|
11
|
+
'proteomics.published',
|
|
12
|
+
'proteomics.published_at',
|
|
13
|
+
'proteomics.published_by',
|
|
14
|
+
'proteomics.published_target',
|
|
15
|
+
'proteomics.published_de_method',
|
|
16
|
+
'proteomics.published_fc_threshold',
|
|
17
|
+
'proteomics.published_p_threshold',
|
|
18
|
+
'proteomics.published_version',
|
|
19
|
+
'proteomics.published_id',
|
|
20
|
+
'proteomics.published_includes_enrichment',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
const SPIKE_SEMTYPE_COLS: Array<[string, string]> = [
|
|
24
|
+
['Protein ID', SEMTYPE.PROTEIN_ID],
|
|
25
|
+
['Gene Name', SEMTYPE.GENE_SYMBOL],
|
|
26
|
+
['log2FC', SEMTYPE.LOG2FC],
|
|
27
|
+
['adj.p-value', SEMTYPE.P_VALUE],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function emit(label: string, value: unknown): void {
|
|
31
|
+
let serialized: string;
|
|
32
|
+
try {
|
|
33
|
+
serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
34
|
+
} catch {
|
|
35
|
+
serialized = String(value);
|
|
36
|
+
}
|
|
37
|
+
const line = `[publish-spike] ${label}: ${serialized}`;
|
|
38
|
+
console.log(line);
|
|
39
|
+
grok.shell.info(line);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildFixtureDataFrame(publishedId: string, nowIso: string): DG.DataFrame {
|
|
43
|
+
const proteinIds = Array.from({length: 10}, (_, i) => `P${String(i + 1).padStart(5, '0')}`);
|
|
44
|
+
const geneNames = Array.from({length: 10}, (_, i) => `GENE${i + 1}`);
|
|
45
|
+
const log2fc = new Float32Array([3.0, 2.5, 2.0, 1.5, 0.5, -0.4, -1.6, -2.2, -2.8, 0.1]);
|
|
46
|
+
const pVals = new Float32Array([0.0001, 0.001, 0.005, 0.02, 0.04, 0.1, 0.001, 0.0005, 0.0001, 0.3]);
|
|
47
|
+
const adjP = new Float32Array([0.001, 0.005, 0.01, 0.04, 0.08, 0.2, 0.005, 0.001, 0.0005, 0.5]);
|
|
48
|
+
const significant = ['up', 'up', 'up', 'up', 'ns', 'ns', 'down', 'down', 'down', 'ns'];
|
|
49
|
+
const direction = ['Enriched in Treatment', 'Enriched in Treatment', 'Enriched in Treatment',
|
|
50
|
+
'Enriched in Treatment', 'Not significant', 'Not significant',
|
|
51
|
+
'Enriched in Control', 'Enriched in Control', 'Enriched in Control', 'Not significant'];
|
|
52
|
+
|
|
53
|
+
const df = DG.DataFrame.fromColumns([
|
|
54
|
+
DG.Column.fromStrings('Protein ID', proteinIds),
|
|
55
|
+
DG.Column.fromStrings('Gene Name', geneNames),
|
|
56
|
+
DG.Column.fromFloat32Array('log2FC', log2fc),
|
|
57
|
+
DG.Column.fromFloat32Array('p-value', pVals),
|
|
58
|
+
DG.Column.fromFloat32Array('adj.p-value', adjP),
|
|
59
|
+
DG.Column.fromStrings('significant', significant),
|
|
60
|
+
DG.Column.fromStrings('direction', direction),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
for (const [colName, sem] of SPIKE_SEMTYPE_COLS)
|
|
64
|
+
df.col(colName)!.semType = sem;
|
|
65
|
+
|
|
66
|
+
df.name = 'spike-source';
|
|
67
|
+
|
|
68
|
+
const groupsPayload = {
|
|
69
|
+
group1: {name: 'Control', columns: []},
|
|
70
|
+
group2: {name: 'Treatment', columns: []},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const tagValues: Record<string, string> = {
|
|
74
|
+
'proteomics.source': 'generic',
|
|
75
|
+
'proteomics.de_method': 'limma',
|
|
76
|
+
'proteomics.de_complete': 'true',
|
|
77
|
+
'proteomics.groups': JSON.stringify(groupsPayload),
|
|
78
|
+
'proteomics.published': 'true',
|
|
79
|
+
'proteomics.published_at': nowIso,
|
|
80
|
+
'proteomics.published_by': 'spike-operator',
|
|
81
|
+
'proteomics.published_target': 'SPIKE-TARGET-A',
|
|
82
|
+
'proteomics.published_de_method': 'limma',
|
|
83
|
+
'proteomics.published_fc_threshold': '1.0',
|
|
84
|
+
'proteomics.published_p_threshold': '0.05',
|
|
85
|
+
'proteomics.published_version': '1',
|
|
86
|
+
'proteomics.published_id': publishedId,
|
|
87
|
+
'proteomics.published_includes_enrichment': 'false',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
for (const [k, v] of Object.entries(tagValues))
|
|
91
|
+
df.setTag(k, v);
|
|
92
|
+
|
|
93
|
+
return df;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function summarizeViewers(): Array<Record<string, unknown>> {
|
|
97
|
+
const out: Array<Record<string, unknown>> = [];
|
|
98
|
+
const tv = grok.shell.tv;
|
|
99
|
+
if (!tv) return out;
|
|
100
|
+
for (const v of tv.viewers) {
|
|
101
|
+
let props: any = null;
|
|
102
|
+
try {
|
|
103
|
+
props = (v as any).getOptions ? (v as any).getOptions()?.look ?? (v as any).getOptions() : null;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
props = `getOptions-threw:${(e as Error)?.message ?? e}`;
|
|
106
|
+
}
|
|
107
|
+
let xCol: unknown;
|
|
108
|
+
let yCol: unknown;
|
|
109
|
+
let colorCol: unknown;
|
|
110
|
+
try { xCol = (v as any).props?.xColumnName; } catch { /* swallow */ }
|
|
111
|
+
try { yCol = (v as any).props?.yColumnName; } catch { /* swallow */ }
|
|
112
|
+
try { colorCol = (v as any).props?.colorColumnName; } catch { /* swallow */ }
|
|
113
|
+
out.push({
|
|
114
|
+
type: v.type,
|
|
115
|
+
xColumnName: xCol,
|
|
116
|
+
yColumnName: yCol,
|
|
117
|
+
colorColumnName: colorCol,
|
|
118
|
+
props,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function safeDelete(label: string, fn: () => Promise<void>): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
await fn();
|
|
127
|
+
emit(`cleanup:${label}`, 'ok');
|
|
128
|
+
} catch (e) {
|
|
129
|
+
emit(`cleanup:${label}:error`, `${(e as Error)?.message ?? e}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
category('Publishing-Spike', () => {
|
|
134
|
+
test('save+open roundtrip — enumerate survivors', async () => {
|
|
135
|
+
const ts = Date.now();
|
|
136
|
+
const publishedId = `spike-pub-${ts}`;
|
|
137
|
+
const nowIso = new Date(ts).toISOString();
|
|
138
|
+
const projectName = `spike-publish-roundtrip-${ts}`;
|
|
139
|
+
|
|
140
|
+
emit('boot', {projectName, publishedId, nowIso});
|
|
141
|
+
|
|
142
|
+
// (1) Build the fixture DataFrame and prime tags + semTypes.
|
|
143
|
+
const df = buildFixtureDataFrame(publishedId, nowIso);
|
|
144
|
+
|
|
145
|
+
// (2) Open in a TableView and add a scatter plot mimicking the volcano shape.
|
|
146
|
+
const tv = grok.shell.addTableView(df);
|
|
147
|
+
await delay(100);
|
|
148
|
+
let sp: DG.ScatterPlotViewer | null = null;
|
|
149
|
+
try {
|
|
150
|
+
const tvAny = tv as any;
|
|
151
|
+
if (typeof tvAny.scatterPlot === 'function') {
|
|
152
|
+
sp = tvAny.scatterPlot({x: 'log2FC', y: 'adj.p-value'});
|
|
153
|
+
} else {
|
|
154
|
+
sp = DG.Viewer.scatterPlot(df, {x: 'log2FC', y: 'adj.p-value'});
|
|
155
|
+
let already = false;
|
|
156
|
+
try { already = Array.from(tv.viewers).includes(sp as any); } catch { /* ignore */ }
|
|
157
|
+
if (sp && !already) tv.addViewer(sp as DG.Viewer);
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
emit('scatter-add-error', `${(e as Error)?.message ?? e}`);
|
|
161
|
+
}
|
|
162
|
+
await delay(100);
|
|
163
|
+
|
|
164
|
+
// (3) Create Project, set TWO options entries (A4 probe), attach children.
|
|
165
|
+
const project = DG.Project.create();
|
|
166
|
+
project.name = projectName;
|
|
167
|
+
try {
|
|
168
|
+
(project as any).options['proteomics.superseded_by'] = 'dummy-id-A';
|
|
169
|
+
(project as any).options['custom_option'] = 'hello';
|
|
170
|
+
} catch (e) {
|
|
171
|
+
emit('project-options-set-error', `${(e as Error)?.message ?? e}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tableInfo = tv.dataFrame.getTableInfo();
|
|
175
|
+
const layoutInfo = tv.getInfo();
|
|
176
|
+
project.addChild(tableInfo);
|
|
177
|
+
project.addChild(layoutInfo);
|
|
178
|
+
|
|
179
|
+
let tableInfoId: string | null = null;
|
|
180
|
+
let projectId: string | null = null;
|
|
181
|
+
let savedSuccessfully = false;
|
|
182
|
+
try {
|
|
183
|
+
await grok.dapi.tables.uploadDataFrame(tv.dataFrame);
|
|
184
|
+
await grok.dapi.tables.save(tableInfo);
|
|
185
|
+
tableInfoId = tableInfo.id;
|
|
186
|
+
await grok.dapi.views.save(layoutInfo);
|
|
187
|
+
await grok.dapi.projects.save(project);
|
|
188
|
+
projectId = project.id;
|
|
189
|
+
savedSuccessfully = true;
|
|
190
|
+
emit('save:ok', {projectId, tableInfoId});
|
|
191
|
+
} catch (e) {
|
|
192
|
+
emit('save:error', `${(e as Error)?.message ?? e}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!savedSuccessfully || !projectId) {
|
|
196
|
+
emit('halt', 'save sequence failed — skipping reopen and downstream probes');
|
|
197
|
+
try { grok.shell.closeAll(); } catch { /* noop */ }
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// (4) Capture id, close shell, find + open.
|
|
202
|
+
try { grok.shell.closeAll(); } catch (e) { emit('closeAll-error', `${(e as Error)?.message ?? e}`); }
|
|
203
|
+
await delay(100);
|
|
204
|
+
|
|
205
|
+
let reopenedProject: DG.Project | null = null;
|
|
206
|
+
try {
|
|
207
|
+
reopenedProject = await grok.dapi.projects.find(projectId);
|
|
208
|
+
await reopenedProject.open();
|
|
209
|
+
} catch (e) {
|
|
210
|
+
emit('reopen:error', `${(e as Error)?.message ?? e}`);
|
|
211
|
+
}
|
|
212
|
+
if (!reopenedProject) {
|
|
213
|
+
emit('halt', 'reopen failed — skipping enumeration');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await awaitCheck(
|
|
219
|
+
() => !!grok.shell.tv && !!grok.shell.tv.dataFrame,
|
|
220
|
+
'TableView did not materialize after Project.open()',
|
|
221
|
+
5000,
|
|
222
|
+
);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
emit('await-tv-error', `${(e as Error)?.message ?? e}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// (5) Enumerate survivors.
|
|
228
|
+
const reTv = grok.shell.tv;
|
|
229
|
+
const reDf = reTv?.dataFrame ?? null;
|
|
230
|
+
|
|
231
|
+
emit('df.name survived', reDf?.name ?? '(no dataFrame)');
|
|
232
|
+
|
|
233
|
+
if (reDf) {
|
|
234
|
+
const tagSurvival: Record<string, string | null> = {};
|
|
235
|
+
for (const k of SPIKE_TAG_KEYS)
|
|
236
|
+
tagSurvival[k] = reDf.getTag(k) ?? null;
|
|
237
|
+
emit('proteomics.* tags survived', tagSurvival);
|
|
238
|
+
|
|
239
|
+
const semTypeSurvival: Record<string, string | null> = {};
|
|
240
|
+
for (const [colName] of SPIKE_SEMTYPE_COLS) {
|
|
241
|
+
const c = reDf.col(colName);
|
|
242
|
+
semTypeSurvival[colName] = c ? (c.semType ?? null) : '(column missing)';
|
|
243
|
+
}
|
|
244
|
+
emit('Proteomics-* semTypes survived', semTypeSurvival);
|
|
245
|
+
|
|
246
|
+
const allCols = reDf.columns.toList().map((c) => ({name: c.name, type: c.type, semType: c.semType ?? null}));
|
|
247
|
+
emit('reopened columns shape', allCols);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// project.options enumeration (A4)
|
|
251
|
+
try {
|
|
252
|
+
const opts = (reopenedProject as any).options ?? {};
|
|
253
|
+
const o1 = opts['proteomics.superseded_by'];
|
|
254
|
+
const o2 = opts['custom_option'];
|
|
255
|
+
let optsKeys: string[] = [];
|
|
256
|
+
try { optsKeys = Object.keys(opts); } catch { /* swallow */ }
|
|
257
|
+
emit('project.options', {
|
|
258
|
+
'proteomics.superseded_by': o1 ?? null,
|
|
259
|
+
'custom_option': o2 ?? null,
|
|
260
|
+
keys: optsKeys,
|
|
261
|
+
rawJson: (() => { try { return JSON.stringify(opts); } catch { return '(unstringifiable)'; } })(),
|
|
262
|
+
});
|
|
263
|
+
} catch (e) {
|
|
264
|
+
emit('project.options:error', `${(e as Error)?.message ?? e}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Viewer survival + bindings (A6)
|
|
268
|
+
emit('viewers after reopen', summarizeViewers());
|
|
269
|
+
|
|
270
|
+
// permissions.get shape (A1)
|
|
271
|
+
try {
|
|
272
|
+
const perm: any = await grok.dapi.permissions.get(reopenedProject);
|
|
273
|
+
let keys: string[] = [];
|
|
274
|
+
try { keys = Object.keys(perm); } catch { /* swallow */ }
|
|
275
|
+
let permJson = '(unstringifiable)';
|
|
276
|
+
try { permJson = JSON.stringify(perm); } catch { /* swallow */ }
|
|
277
|
+
emit('permissions.get keys', keys);
|
|
278
|
+
emit('permissions.get json', permJson);
|
|
279
|
+
} catch (e) {
|
|
280
|
+
emit('permissions.get:error', `${(e as Error)?.message ?? e}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// (6) Space-inheritance probe (A2).
|
|
284
|
+
let umbrellaSpace: any = null;
|
|
285
|
+
let inheritedChildSpace: any = null;
|
|
286
|
+
try {
|
|
287
|
+
umbrellaSpace = await (grok.dapi as any).spaces.createRootSpace(`Spike-Reviews-${ts}`);
|
|
288
|
+
emit('umbrella-space', {id: umbrellaSpace.id, friendlyName: (umbrellaSpace as any).friendlyName});
|
|
289
|
+
const umbrellaClient = (grok.dapi as any).spaces.id(umbrellaSpace.id);
|
|
290
|
+
inheritedChildSpace = await umbrellaClient.addSubspace(`Spike-Review-Inherit-${ts}`);
|
|
291
|
+
emit('inherited-child-space', {id: inheritedChildSpace.id, friendlyName: (inheritedChildSpace as any).friendlyName});
|
|
292
|
+
const inheritedClient = (grok.dapi as any).spaces.id(inheritedChildSpace.id);
|
|
293
|
+
try {
|
|
294
|
+
await inheritedClient.addEntity(reopenedProject.id);
|
|
295
|
+
emit('inherited-child:addEntity', 'ok');
|
|
296
|
+
} catch (e) {
|
|
297
|
+
emit('inherited-child:addEntity:error', `${(e as Error)?.message ?? e}`);
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const me = await grok.dapi.users.current();
|
|
301
|
+
const myGroup: any = (me as any).group ?? null;
|
|
302
|
+
if (myGroup) {
|
|
303
|
+
await grok.dapi.permissions.grant(umbrellaSpace, myGroup, false);
|
|
304
|
+
emit('umbrella-space:grant-view', {group: (myGroup as any).friendlyName ?? (myGroup as any).name ?? '(unnamed)'});
|
|
305
|
+
} else {
|
|
306
|
+
emit('umbrella-space:grant-skipped', 'no user.group found');
|
|
307
|
+
}
|
|
308
|
+
} catch (e) {
|
|
309
|
+
emit('umbrella-space:grant:error', `${(e as Error)?.message ?? e}`);
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const childPerm = await grok.dapi.permissions.get(reopenedProject);
|
|
313
|
+
let cpKeys: string[] = [];
|
|
314
|
+
try { cpKeys = Object.keys(childPerm as any); } catch { /* swallow */ }
|
|
315
|
+
emit('permissions.get(childProject) after umbrella-grant', {
|
|
316
|
+
keys: cpKeys,
|
|
317
|
+
json: (() => { try { return JSON.stringify(childPerm); } catch { return '(unstringifiable)'; } })(),
|
|
318
|
+
});
|
|
319
|
+
} catch (e) {
|
|
320
|
+
emit('childProject:permissions.get:error', `${(e as Error)?.message ?? e}`);
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
emit('space-probe:error', `${(e as Error)?.message ?? e}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// (7) Smart-filter syntax probe (A8).
|
|
327
|
+
try {
|
|
328
|
+
const likeList: any = await grok.dapi.projects.filter('name like "spike-publish-roundtrip-%"').list();
|
|
329
|
+
emit('filter "like" result', {count: Array.isArray(likeList) ? likeList.length : '(non-array)',
|
|
330
|
+
firstName: Array.isArray(likeList) && likeList.length > 0 ? (likeList[0] as any).name ?? null : null});
|
|
331
|
+
} catch (e) {
|
|
332
|
+
emit('filter:like:error', `${(e as Error)?.message ?? e}`);
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const containsList: any = await grok.dapi.projects.filter('name contains "spike-publish-roundtrip"').list();
|
|
336
|
+
emit('filter "contains" result', {count: Array.isArray(containsList) ? containsList.length : '(non-array)',
|
|
337
|
+
firstName: Array.isArray(containsList) && containsList.length > 0 ? (containsList[0] as any).name ?? null : null});
|
|
338
|
+
} catch (e) {
|
|
339
|
+
emit('filter:contains:error', `${(e as Error)?.message ?? e}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// (8) Delete-cascade probe (A3).
|
|
343
|
+
const savedTableInfoId = tableInfoId;
|
|
344
|
+
try {
|
|
345
|
+
await grok.dapi.projects.delete(reopenedProject);
|
|
346
|
+
emit('projects.delete', 'ok');
|
|
347
|
+
} catch (e) {
|
|
348
|
+
emit('projects.delete:error', `${(e as Error)?.message ?? e}`);
|
|
349
|
+
}
|
|
350
|
+
if (savedTableInfoId) {
|
|
351
|
+
try {
|
|
352
|
+
const t = await grok.dapi.tables.find(savedTableInfoId);
|
|
353
|
+
emit('tables.find(tableInfoId) after project.delete', {
|
|
354
|
+
resolved: !!t,
|
|
355
|
+
name: t ? (t as any).name ?? null : null,
|
|
356
|
+
});
|
|
357
|
+
} catch (e) {
|
|
358
|
+
emit('tables.find:after-delete:error', `${(e as Error)?.message ?? e}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// (9) Cleanup — idempotent.
|
|
363
|
+
if (inheritedChildSpace)
|
|
364
|
+
await safeDelete('inherited-child-space', () => (grok.dapi as any).spaces.delete(inheritedChildSpace));
|
|
365
|
+
if (umbrellaSpace)
|
|
366
|
+
await safeDelete('umbrella-space', () => (grok.dapi as any).spaces.delete(umbrellaSpace));
|
|
367
|
+
// Best-effort: delete the project again in case the A3 probe earlier already ran but didn't cascade.
|
|
368
|
+
await safeDelete('project (idempotent)', async () => {
|
|
369
|
+
try {
|
|
370
|
+
const stillThere = await grok.dapi.projects.find(projectId!);
|
|
371
|
+
if (stillThere) await grok.dapi.projects.delete(stillThere);
|
|
372
|
+
} catch { /* not found is fine */ }
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
emit('done', 'spike enumeration complete — read output above');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as DG from 'datagrok-api/dg';
|
|
2
|
+
import {category, test, expect} from '@datagrok-libraries/test/src/test';
|
|
3
|
+
import {
|
|
4
|
+
computeMA,
|
|
5
|
+
computeCV,
|
|
6
|
+
computeLoessTrend,
|
|
7
|
+
createMissingnessMatrix,
|
|
8
|
+
unpivotIntensities,
|
|
9
|
+
computeMissingBarData,
|
|
10
|
+
getIntensityColumns,
|
|
11
|
+
} from '../viewers/qc-computations';
|
|
12
|
+
import {GroupAssignment} from '../analysis/experiment-setup';
|
|
13
|
+
import {SEMTYPE} from '../utils/proteomics-types';
|
|
14
|
+
|
|
15
|
+
category('QC Dashboard', () => {
|
|
16
|
+
test('getIntensityColumns', async () => {
|
|
17
|
+
const col1 = DG.Column.fromFloat32Array('log2(sample1)', new Float32Array([1, 2]));
|
|
18
|
+
col1.semType = SEMTYPE.INTENSITY;
|
|
19
|
+
const col2 = DG.Column.fromFloat32Array('other', new Float32Array([3, 4]));
|
|
20
|
+
const col3 = DG.Column.fromFloat32Array('log2(sample2)', new Float32Array([5, 6]));
|
|
21
|
+
col3.semType = SEMTYPE.INTENSITY;
|
|
22
|
+
const df = DG.DataFrame.fromColumns([col1, col2, col3]);
|
|
23
|
+
|
|
24
|
+
const result = getIntensityColumns(df);
|
|
25
|
+
expect(result.length, 2);
|
|
26
|
+
expect(result[0], 'log2(sample1)');
|
|
27
|
+
expect(result[1], 'log2(sample2)');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('MA computation', async () => {
|
|
31
|
+
// 3 rows, 4 intensity columns: 2 per group
|
|
32
|
+
const c1 = DG.Column.fromFloat32Array('log2(ctrl1)', new Float32Array([10, 5, 8]));
|
|
33
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
34
|
+
const c2 = DG.Column.fromFloat32Array('log2(ctrl2)', new Float32Array([12, 7, 6]));
|
|
35
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
36
|
+
const c3 = DG.Column.fromFloat32Array('log2(treat1)', new Float32Array([8, 3, DG.FLOAT_NULL]));
|
|
37
|
+
c3.semType = SEMTYPE.INTENSITY;
|
|
38
|
+
const c4 = DG.Column.fromFloat32Array('log2(treat2)', new Float32Array([6, 5, DG.FLOAT_NULL]));
|
|
39
|
+
c4.semType = SEMTYPE.INTENSITY;
|
|
40
|
+
|
|
41
|
+
const df = DG.DataFrame.fromColumns([c1, c2, c3, c4]);
|
|
42
|
+
const groups: GroupAssignment = {
|
|
43
|
+
group1: {name: 'Control', columns: ['log2(ctrl1)', 'log2(ctrl2)']},
|
|
44
|
+
group2: {name: 'Treatment', columns: ['log2(treat1)', 'log2(treat2)']},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
computeMA(df, groups);
|
|
48
|
+
|
|
49
|
+
const mCol = df.col('M')!;
|
|
50
|
+
const aCol = df.col('A')!;
|
|
51
|
+
|
|
52
|
+
// Row 0: g1Mean = (10+12)/2 = 11, g2Mean = (8+6)/2 = 7
|
|
53
|
+
// M = g2 - g1 = 7 - 11 = -4, A = (11+7)/2 = 9
|
|
54
|
+
expect(Math.abs(mCol.get(0) - (-4)) < 0.01, true);
|
|
55
|
+
expect(Math.abs(aCol.get(0) - 9) < 0.01, true);
|
|
56
|
+
|
|
57
|
+
// Row 2: all null in group2 -> M and A should be null
|
|
58
|
+
expect(mCol.isNone(2), true);
|
|
59
|
+
expect(aCol.isNone(2), true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('CV computation', async () => {
|
|
63
|
+
// 2 rows, 3 columns for one group
|
|
64
|
+
// Row 0: all identical values (2, 2, 2) -> CV = 0
|
|
65
|
+
// Row 1: varied values -> CV > 0
|
|
66
|
+
const c1 = DG.Column.fromFloat32Array('log2(s1)', new Float32Array([2, 3]));
|
|
67
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
68
|
+
const c2 = DG.Column.fromFloat32Array('log2(s2)', new Float32Array([2, 5]));
|
|
69
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
70
|
+
const c3 = DG.Column.fromFloat32Array('log2(s3)', new Float32Array([2, 7]));
|
|
71
|
+
c3.semType = SEMTYPE.INTENSITY;
|
|
72
|
+
|
|
73
|
+
const df = DG.DataFrame.fromColumns([c1, c2, c3]);
|
|
74
|
+
computeCV(df, ['log2(s1)', 'log2(s2)', 'log2(s3)'], 'CV', 'Mean');
|
|
75
|
+
|
|
76
|
+
const cvCol = df.col('CV')!;
|
|
77
|
+
const meanCol = df.col('Mean')!;
|
|
78
|
+
|
|
79
|
+
// Row 0: all log2 values = 2, raw = 2^2 = 4 for all. sd = 0, CV = 0. Mean = 4.
|
|
80
|
+
expect(Math.abs(cvCol.get(0)) < 0.001, true);
|
|
81
|
+
expect(Math.abs(meanCol.get(0) - 4) < 0.01, true);
|
|
82
|
+
|
|
83
|
+
// Row 1: varied values -> CV > 0
|
|
84
|
+
expect(cvCol.get(1) > 0, true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('Loess/moving-average trend', async () => {
|
|
88
|
+
// Create a DataFrame with M and A columns, 20+ rows
|
|
89
|
+
const n = 25;
|
|
90
|
+
// Use a seeded LCG instead of Math.random so the smoothness assertion
|
|
91
|
+
// below isn't flaky on adversarial RNG draws.
|
|
92
|
+
let seed = 12345;
|
|
93
|
+
const rand = () => {
|
|
94
|
+
seed = (seed * 1664525 + 1013904223) >>> 0;
|
|
95
|
+
return seed / 0xFFFFFFFF;
|
|
96
|
+
};
|
|
97
|
+
const aValues = new Float32Array(n);
|
|
98
|
+
const mValues = new Float32Array(n);
|
|
99
|
+
for (let i = 0; i < n; i++) {
|
|
100
|
+
aValues[i] = i + 1; // A from 1 to 25
|
|
101
|
+
mValues[i] = 0.5 * Math.sin(i / 5) + (rand() - 0.5) * 0.3; // pattern + seeded noise
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const df = DG.DataFrame.fromColumns([
|
|
105
|
+
DG.Column.fromFloat32Array('A', aValues),
|
|
106
|
+
DG.Column.fromFloat32Array('M', mValues),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
computeLoessTrend(df, 0.5);
|
|
110
|
+
|
|
111
|
+
// Assert MA_trend column exists
|
|
112
|
+
const trendCol = df.col('MA_trend');
|
|
113
|
+
expect(trendCol !== null, true);
|
|
114
|
+
|
|
115
|
+
// Assert not all null
|
|
116
|
+
let nonNullCount = 0;
|
|
117
|
+
for (let i = 0; i < n; i++) {
|
|
118
|
+
if (!trendCol!.isNone(i))
|
|
119
|
+
nonNullCount++;
|
|
120
|
+
}
|
|
121
|
+
expect(nonNullCount > 0, true);
|
|
122
|
+
|
|
123
|
+
// Assert trend is smoother than raw M: variance(trend) < variance(M)
|
|
124
|
+
let sumM = 0; let sumM2 = 0; let countM = 0;
|
|
125
|
+
let sumT = 0; let sumT2 = 0; let countT = 0;
|
|
126
|
+
for (let i = 0; i < n; i++) {
|
|
127
|
+
const m = mValues[i];
|
|
128
|
+
sumM += m; sumM2 += m * m; countM++;
|
|
129
|
+
if (!trendCol!.isNone(i)) {
|
|
130
|
+
const t = trendCol!.get(i) as number;
|
|
131
|
+
sumT += t; sumT2 += t * t; countT++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const varM = (sumM2 / countM) - (sumM / countM) * (sumM / countM);
|
|
135
|
+
const varT = (sumT2 / countT) - (sumT / countT) * (sumT / countT);
|
|
136
|
+
expect(varT < varM, true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('Missingness matrix', async () => {
|
|
140
|
+
const c1 = DG.Column.fromFloat32Array('log2(s1)', new Float32Array([5.0, DG.FLOAT_NULL]));
|
|
141
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
142
|
+
const c2 = DG.Column.fromFloat32Array('log2(s2)', new Float32Array([DG.FLOAT_NULL, 3.0]));
|
|
143
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
144
|
+
|
|
145
|
+
const df = DG.DataFrame.fromColumns([c1, c2]);
|
|
146
|
+
const result = createMissingnessMatrix(df, ['log2(s1)', 'log2(s2)']);
|
|
147
|
+
|
|
148
|
+
expect(result.rowCount, 2);
|
|
149
|
+
expect(result.name, 'Missing Values');
|
|
150
|
+
|
|
151
|
+
// Row 0: s1=present(1), s2=missing(0)
|
|
152
|
+
const rc1 = result.col('log2(s1)')!;
|
|
153
|
+
const rc2 = result.col('log2(s2)')!;
|
|
154
|
+
expect(rc1.get(0), 1);
|
|
155
|
+
expect(rc2.get(0), 0);
|
|
156
|
+
// Row 1: s1=missing(0), s2=present(1)
|
|
157
|
+
expect(rc1.get(1), 0);
|
|
158
|
+
expect(rc2.get(1), 1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('Unpivot intensities', async () => {
|
|
162
|
+
const idCol = DG.Column.fromStrings('Protein ID', ['P0', 'P1']);
|
|
163
|
+
idCol.semType = SEMTYPE.PROTEIN_ID;
|
|
164
|
+
const c1 = DG.Column.fromFloat32Array('log2(s1)', new Float32Array([10, DG.FLOAT_NULL]));
|
|
165
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
166
|
+
const c2 = DG.Column.fromFloat32Array('log2(s2)', new Float32Array([20, 30]));
|
|
167
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
168
|
+
|
|
169
|
+
const df = DG.DataFrame.fromColumns([idCol, c1, c2]);
|
|
170
|
+
const result = unpivotIntensities(df, ['log2(s1)', 'log2(s2)']);
|
|
171
|
+
|
|
172
|
+
expect(result.name, 'Intensity Distributions');
|
|
173
|
+
// 3 non-null values: P0/s1(10), P0/s2(20), P1/s2(30)
|
|
174
|
+
expect(result.rowCount, 3);
|
|
175
|
+
expect(result.col('ProteinId') !== null, true);
|
|
176
|
+
expect(result.col('Sample') !== null, true);
|
|
177
|
+
expect(result.col('Intensity') !== null, true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('Missing bar data', async () => {
|
|
181
|
+
// 3 rows, 2 intensity columns (1 per group)
|
|
182
|
+
// Col 0: 1 null out of 3 (33.3%)
|
|
183
|
+
// Col 1: 0 null out of 3 (0%)
|
|
184
|
+
const c1 = DG.Column.fromFloat32Array('log2(ctrl1)', new Float32Array([5, DG.FLOAT_NULL, 7]));
|
|
185
|
+
c1.semType = SEMTYPE.INTENSITY;
|
|
186
|
+
const c2 = DG.Column.fromFloat32Array('log2(treat1)', new Float32Array([10, 11, 12]));
|
|
187
|
+
c2.semType = SEMTYPE.INTENSITY;
|
|
188
|
+
|
|
189
|
+
const df = DG.DataFrame.fromColumns([c1, c2]);
|
|
190
|
+
const groups: GroupAssignment = {
|
|
191
|
+
group1: {name: 'Control', columns: ['log2(ctrl1)']},
|
|
192
|
+
group2: {name: 'Treatment', columns: ['log2(treat1)']},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const result = computeMissingBarData(df, ['log2(ctrl1)', 'log2(treat1)'], groups);
|
|
196
|
+
|
|
197
|
+
expect(result.rowCount, 2);
|
|
198
|
+
const pctCol = result.col('MissingPct')!;
|
|
199
|
+
const grpCol = result.col('Group')!;
|
|
200
|
+
|
|
201
|
+
// Row 0 (ctrl1): ~33.3% missing
|
|
202
|
+
expect(Math.abs(pctCol.get(0) - 33.333) < 1, true);
|
|
203
|
+
// Row 1 (treat1): 0% missing
|
|
204
|
+
expect(Math.abs(pctCol.get(1)) < 0.01, true);
|
|
205
|
+
|
|
206
|
+
// Group assignments
|
|
207
|
+
expect(grpCol.get(0), 'Control');
|
|
208
|
+
expect(grpCol.get(1), 'Treatment');
|
|
209
|
+
});
|
|
210
|
+
});
|