nltools 0.6.0.dev0__py3-none-any.whl
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.
- nltools/__init__.py +55 -0
- nltools/algorithms/__init__.py +90 -0
- nltools/algorithms/alignment/__init__.py +21 -0
- nltools/algorithms/alignment/procrustes.py +565 -0
- nltools/algorithms/alignment/srm.py +758 -0
- nltools/algorithms/backends.py +1059 -0
- nltools/algorithms/corrections.py +177 -0
- nltools/algorithms/decoding.py +327 -0
- nltools/algorithms/inference/__init__.py +50 -0
- nltools/algorithms/inference/bootstrap.py +1386 -0
- nltools/algorithms/inference/correlation.py +373 -0
- nltools/algorithms/inference/intersubject.py +422 -0
- nltools/algorithms/inference/isc.py +1554 -0
- nltools/algorithms/inference/matrix.py +602 -0
- nltools/algorithms/inference/one_sample.py +288 -0
- nltools/algorithms/inference/random.py +122 -0
- nltools/algorithms/inference/timeseries.py +347 -0
- nltools/algorithms/inference/two_sample.py +212 -0
- nltools/algorithms/inference/utils.py +58 -0
- nltools/algorithms/inference/validation.py +282 -0
- nltools/algorithms/neighborhoods.py +207 -0
- nltools/algorithms/outliers.py +308 -0
- nltools/algorithms/regression.py +83 -0
- nltools/algorithms/signal.py +303 -0
- nltools/algorithms/similarity.py +234 -0
- nltools/algorithms/validation.py +151 -0
- nltools/cross_validation.py +72 -0
- nltools/data/__init__.py +30 -0
- nltools/data/adjacency/__init__.py +875 -0
- nltools/data/adjacency/io.py +111 -0
- nltools/data/adjacency/modeling.py +569 -0
- nltools/data/adjacency/plotting.py +174 -0
- nltools/data/adjacency/state.py +349 -0
- nltools/data/adjacency/stats.py +596 -0
- nltools/data/adjacency/utils.py +79 -0
- nltools/data/atlases/__init__.py +23 -0
- nltools/data/atlases/labeling.py +158 -0
- nltools/data/atlases/loading.py +76 -0
- nltools/data/atlases/registry.py +96 -0
- nltools/data/atlases/reporting.py +456 -0
- nltools/data/braindata/__init__.py +2170 -0
- nltools/data/braindata/analysis.py +1381 -0
- nltools/data/braindata/bootstrap.py +398 -0
- nltools/data/braindata/io.py +896 -0
- nltools/data/braindata/modeling.py +594 -0
- nltools/data/braindata/plotting.py +501 -0
- nltools/data/braindata/prediction.py +1250 -0
- nltools/data/braindata/utils.py +348 -0
- nltools/data/braindata/validation.py +197 -0
- nltools/data/braindata/viewer.js +266 -0
- nltools/data/braindata/viewer.py +770 -0
- nltools/data/combine.py +27 -0
- nltools/data/designmatrix/__init__.py +1032 -0
- nltools/data/designmatrix/append.py +518 -0
- nltools/data/designmatrix/diagnostics.py +248 -0
- nltools/data/designmatrix/io.py +356 -0
- nltools/data/designmatrix/plotting.py +291 -0
- nltools/data/designmatrix/regressors.py +463 -0
- nltools/data/designmatrix/transforms.py +200 -0
- nltools/data/designmatrix/utils.py +350 -0
- nltools/data/ownership.py +129 -0
- nltools/data/results.py +291 -0
- nltools/data/roc/__init__.py +398 -0
- nltools/data/simulator/__init__.py +927 -0
- nltools/data/simulator/haxby.py +124 -0
- nltools/data/validation.py +83 -0
- nltools/datasets.py +218 -0
- nltools/io/__init__.py +10 -0
- nltools/io/events.py +67 -0
- nltools/io/h5.py +246 -0
- nltools/mask.py +403 -0
- nltools/models/__init__.py +11 -0
- nltools/models/glm.py +543 -0
- nltools/models/results.py +49 -0
- nltools/models/ridge.py +1303 -0
- nltools/models/validation.py +26 -0
- nltools/plotting/__init__.py +32 -0
- nltools/plotting/adjacency.py +421 -0
- nltools/plotting/brain.py +669 -0
- nltools/plotting/decomposition.py +111 -0
- nltools/plotting/prediction.py +110 -0
- nltools/resources/covariates_example.csv +161 -0
- nltools/resources/onsets_example.csv +40 -0
- nltools/templates/__init__.py +51 -0
- nltools/templates/config.py +144 -0
- nltools/templates/fetch.py +260 -0
- nltools/templates/matching.py +183 -0
- nltools/templates/paths.py +106 -0
- nltools/templates/registry.py +25 -0
- nltools/utils.py +230 -0
- nltools/version.py +13 -0
- nltools-0.6.0.dev0.dist-info/METADATA +95 -0
- nltools-0.6.0.dev0.dist-info/RECORD +95 -0
- nltools-0.6.0.dev0.dist-info/WHEEL +4 -0
- nltools-0.6.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// niivue viewer for BrainData.iplot(), driven through anywidget's *standard*
|
|
2
|
+
// model API (get / set / save_changes / on) only — no host-specific protocol
|
|
3
|
+
// (the contrast with ipyniivue, see cosanlab/nltools#455), so the widget works
|
|
4
|
+
// identically in Jupyter and `marimo edit`.
|
|
5
|
+
//
|
|
6
|
+
// niivue itself is pulled from a CDN as an ES module. `_esm` is an ES module, so
|
|
7
|
+
// a top-level `import` is resolved by the browser at render time. The pin keeps
|
|
8
|
+
// the API stable.
|
|
9
|
+
import {
|
|
10
|
+
Niivue,
|
|
11
|
+
NVImage,
|
|
12
|
+
SLICE_TYPE,
|
|
13
|
+
} from "https://esm.sh/@niivue/niivue@0.69.0";
|
|
14
|
+
|
|
15
|
+
// A traitlets `Bytes` trait arrives in JS as a DataView over a shared buffer.
|
|
16
|
+
// niivue wants a standalone ArrayBuffer, so slice out exactly this view's
|
|
17
|
+
// window. Empty bytes (an absent optional volume) yield `null`.
|
|
18
|
+
function bytesToArrayBuffer(dv) {
|
|
19
|
+
if (!dv || dv.byteLength === 0) return null;
|
|
20
|
+
return dv.buffer.slice(dv.byteOffset, dv.byteOffset + dv.byteLength);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// NVImage.new treats NaN cal_min/cal_max as "auto" (percentile-derived). A null
|
|
24
|
+
// trait (window left on auto) maps to NaN; a number passes through.
|
|
25
|
+
function orNaN(x) {
|
|
26
|
+
return x === null || x === undefined ? NaN : x;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A trait or slider value that is really a number (a null/undefined trait or a
|
|
30
|
+
// NaN parse means "leave this edge alone").
|
|
31
|
+
function isNumber(x) {
|
|
32
|
+
return x !== null && x !== undefined && !Number.isNaN(x);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatValue(value) {
|
|
36
|
+
const number = Number(value);
|
|
37
|
+
if (!Number.isFinite(number)) return "—";
|
|
38
|
+
return Number.parseFloat(number.toPrecision(4)).toString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
async render({ model, el }) {
|
|
43
|
+
el.style.width = "100%";
|
|
44
|
+
|
|
45
|
+
// --- DOM: optional threshold controls + the WebGL canvas -------------- //
|
|
46
|
+
let floorInput = null;
|
|
47
|
+
let ceilInput = null;
|
|
48
|
+
let floorValue = null;
|
|
49
|
+
let ceilValue = null;
|
|
50
|
+
const bounds = model.get("slider_bounds") || {};
|
|
51
|
+
// The window is a magnitude window mirrored onto the negative limb, and a
|
|
52
|
+
// floor of zero (or below) admits every zero-valued voxel — the whole
|
|
53
|
+
// volume box outside the mask paints in the negative colormap. Never let
|
|
54
|
+
// the applied floor reach zero, whatever the slider or the model says.
|
|
55
|
+
//
|
|
56
|
+
// The epsilon is the slider's own lower bound, which Python guarantees is
|
|
57
|
+
// strictly positive and at or below the resolved `cal_min`. Deriving it
|
|
58
|
+
// here instead (from the step, say) would let the two drift apart and
|
|
59
|
+
// raise the rendered floor above the window Python resolved.
|
|
60
|
+
const floorEps = Number(bounds.min) > 0 ? Number(bounds.min) : 1e-9;
|
|
61
|
+
const clampFloor = (v) => Math.max(Math.abs(v), floorEps);
|
|
62
|
+
|
|
63
|
+
if (model.get("controls")) {
|
|
64
|
+
const controls = document.createElement("div");
|
|
65
|
+
controls.style.cssText =
|
|
66
|
+
"display:flex;gap:0.75rem;align-items:center;font:12px/1.4 system-ui," +
|
|
67
|
+
"sans-serif;padding:4px 2px;flex-wrap:wrap";
|
|
68
|
+
|
|
69
|
+
const mkRange = (labelText, value) => {
|
|
70
|
+
const wrap = document.createElement("label");
|
|
71
|
+
wrap.style.cssText =
|
|
72
|
+
"display:flex;gap:0.4rem;align-items:center;flex:1 1 200px";
|
|
73
|
+
const span = document.createElement("span");
|
|
74
|
+
span.textContent = labelText;
|
|
75
|
+
const low = document.createElement("span");
|
|
76
|
+
low.textContent = formatValue(bounds.min ?? 0);
|
|
77
|
+
const high = document.createElement("span");
|
|
78
|
+
high.textContent = formatValue(bounds.max ?? 1);
|
|
79
|
+
const input = document.createElement("input");
|
|
80
|
+
input.type = "range";
|
|
81
|
+
input.min = bounds.min ?? 0;
|
|
82
|
+
input.max = bounds.max ?? 1;
|
|
83
|
+
input.step = bounds.step ?? 0.01;
|
|
84
|
+
input.value = value ?? bounds.min ?? 0;
|
|
85
|
+
input.style.flex = "1";
|
|
86
|
+
const output = document.createElement("output");
|
|
87
|
+
output.textContent = formatValue(input.value);
|
|
88
|
+
output.style.cssText = "min-width:6ch;text-align:right;font-variant-numeric:tabular-nums";
|
|
89
|
+
wrap.append(span, low, input, high, output);
|
|
90
|
+
controls.appendChild(wrap);
|
|
91
|
+
return [input, output];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
[floorInput, floorValue] = mkRange("min", bounds.value_low);
|
|
95
|
+
[ceilInput, ceilValue] = mkRange("max", bounds.value_high);
|
|
96
|
+
el.appendChild(controls);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const height = model.get("height") || 400;
|
|
100
|
+
// niivue's resize observer sizes the canvas from its *parent* and rewrites
|
|
101
|
+
// the canvas's own inline height to 100%, so the requested height has to
|
|
102
|
+
// live on a wrapper. Without it the viewer collapses to the parent's
|
|
103
|
+
// natural height (~150px) in any host that doesn't size the widget for
|
|
104
|
+
// us — static/exported pages, iframes, plain Jupyter output areas.
|
|
105
|
+
const canvasWrap = document.createElement("div");
|
|
106
|
+
canvasWrap.style.cssText = `width:100%;height:${height}px`;
|
|
107
|
+
const canvas = document.createElement("canvas");
|
|
108
|
+
canvas.style.cssText = "width:100%;height:100%;display:block";
|
|
109
|
+
canvasWrap.appendChild(canvas);
|
|
110
|
+
el.appendChild(canvasWrap);
|
|
111
|
+
|
|
112
|
+
// --- niivue instance --------------------------------------------------- //
|
|
113
|
+
const nv = new Niivue({
|
|
114
|
+
isColorbar: model.get("colorbar"),
|
|
115
|
+
...(model.get("niivue_opts") || {}),
|
|
116
|
+
});
|
|
117
|
+
await nv.attachToCanvas(canvas);
|
|
118
|
+
|
|
119
|
+
// Index of the stat map inside nv.volumes (set by loadStack); the threshold
|
|
120
|
+
// controls drive this volume's window.
|
|
121
|
+
let statmapIdx = -1;
|
|
122
|
+
|
|
123
|
+
// Build the volume stack [background?, statmap, atlas?] from the byte
|
|
124
|
+
// traits + display params. Rebuilt whenever any volume's bytes change.
|
|
125
|
+
async function loadStack() {
|
|
126
|
+
while (nv.volumes.length) nv.removeVolumeByIndex(0);
|
|
127
|
+
statmapIdx = -1;
|
|
128
|
+
|
|
129
|
+
const bg = bytesToArrayBuffer(model.get("bg_bytes"));
|
|
130
|
+
if (bg) {
|
|
131
|
+
const bgVol = await NVImage.new(bg, "background.nii.gz", "gray");
|
|
132
|
+
bgVol.colorbarVisible = false;
|
|
133
|
+
nv.addVolume(bgVol);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const stat = bytesToArrayBuffer(model.get("statmap_bytes"));
|
|
137
|
+
if (stat) {
|
|
138
|
+
const p = model.get("statmap") || {};
|
|
139
|
+
const vol = await NVImage.new(
|
|
140
|
+
stat,
|
|
141
|
+
(p.name || "statmap") + ".nii.gz",
|
|
142
|
+
p.colormap || "warm",
|
|
143
|
+
p.opacity ?? 1.0,
|
|
144
|
+
null,
|
|
145
|
+
orNaN(model.get("cal_min")),
|
|
146
|
+
orNaN(model.get("cal_max")),
|
|
147
|
+
);
|
|
148
|
+
if (Number.isFinite(vol.cal_min)) vol.cal_min = clampFloor(vol.cal_min);
|
|
149
|
+
if (p.colormap_negative) vol.colormapNegative = p.colormap_negative;
|
|
150
|
+
vol.cal_minNeg = orNaN(model.get("cal_min_neg"));
|
|
151
|
+
vol.cal_maxNeg = orNaN(model.get("cal_max_neg"));
|
|
152
|
+
vol.colorbarVisible = true;
|
|
153
|
+
statmapIdx = nv.volumes.length;
|
|
154
|
+
nv.addVolume(vol);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const atlas = bytesToArrayBuffer(model.get("atlas_bytes"));
|
|
158
|
+
if (atlas) {
|
|
159
|
+
const p = model.get("statmap") || {};
|
|
160
|
+
const vol = await NVImage.new(
|
|
161
|
+
atlas,
|
|
162
|
+
(model.get("atlas_name") || "atlas") + ".nii.gz",
|
|
163
|
+
"",
|
|
164
|
+
p.opacity ?? 1.0,
|
|
165
|
+
);
|
|
166
|
+
const lut = model.get("atlas_lut");
|
|
167
|
+
if (lut && lut.labels) vol.setColormapLabel(lut);
|
|
168
|
+
vol.colorbarVisible = false;
|
|
169
|
+
nv.addVolume(vol);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
nv.opts.atlasOutline = model.get("atlas_outline") || 0;
|
|
173
|
+
nv.updateGLVolume();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await loadStack();
|
|
177
|
+
nv.setSliceType(SLICE_TYPE[model.get("slice_type")] ?? SLICE_TYPE.MULTIPLANAR);
|
|
178
|
+
|
|
179
|
+
// --- threshold controls -> niivue + Python ---------------------------- //
|
|
180
|
+
// Applies the clamped window to niivue and returns it, so the callers that
|
|
181
|
+
// write back to Python send what was rendered rather than what was dragged.
|
|
182
|
+
function applyWindow() {
|
|
183
|
+
if (statmapIdx < 0) return null;
|
|
184
|
+
const vol = nv.volumes[statmapIdx];
|
|
185
|
+
const loRaw = floorInput ? parseFloat(floorInput.value) : model.get("cal_min");
|
|
186
|
+
const hiRaw = ceilInput ? parseFloat(ceilInput.value) : model.get("cal_max");
|
|
187
|
+
// Both edges are magnitudes: the floor stays above zero and the ceiling
|
|
188
|
+
// stays above the floor.
|
|
189
|
+
const lo = isNumber(loRaw) ? clampFloor(loRaw) : null;
|
|
190
|
+
const floor = lo === null ? vol.cal_min : lo;
|
|
191
|
+
const hi = isNumber(hiRaw)
|
|
192
|
+
? Math.max(Math.abs(hiRaw), floor + floorEps)
|
|
193
|
+
: null;
|
|
194
|
+
if (lo !== null) vol.cal_min = lo;
|
|
195
|
+
if (hi !== null) vol.cal_max = hi;
|
|
196
|
+
if (model.get("mirror_negative")) {
|
|
197
|
+
if (hi !== null) vol.cal_minNeg = -hi;
|
|
198
|
+
if (lo !== null) vol.cal_maxNeg = -lo;
|
|
199
|
+
} else if (lo !== null) {
|
|
200
|
+
vol.cal_maxNeg = -lo;
|
|
201
|
+
}
|
|
202
|
+
nv.updateGLVolume();
|
|
203
|
+
return { lo, hi };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (floorInput) {
|
|
207
|
+
floorInput.addEventListener("input", () => {
|
|
208
|
+
const applied = applyWindow();
|
|
209
|
+
const lo = applied && applied.lo !== null
|
|
210
|
+
? applied.lo
|
|
211
|
+
: parseFloat(floorInput.value);
|
|
212
|
+
floorValue.textContent = formatValue(lo);
|
|
213
|
+
model.set("cal_min", lo);
|
|
214
|
+
model.save_changes();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (ceilInput) {
|
|
218
|
+
ceilInput.addEventListener("input", () => {
|
|
219
|
+
const applied = applyWindow();
|
|
220
|
+
const hi = applied && applied.hi !== null
|
|
221
|
+
? applied.hi
|
|
222
|
+
: parseFloat(ceilInput.value);
|
|
223
|
+
ceilValue.textContent = formatValue(hi);
|
|
224
|
+
model.set("cal_max", hi);
|
|
225
|
+
model.save_changes();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- Python -> JS reactivity ------------------------------------------ //
|
|
230
|
+
const onBytes = () => loadStack();
|
|
231
|
+
model.on("change:statmap_bytes", onBytes);
|
|
232
|
+
model.on("change:bg_bytes", onBytes);
|
|
233
|
+
model.on("change:atlas_bytes", onBytes);
|
|
234
|
+
|
|
235
|
+
model.on("change:cal_min", () => {
|
|
236
|
+
if (floorInput) floorInput.value = model.get("cal_min") ?? floorInput.value;
|
|
237
|
+
if (floorValue) floorValue.textContent = formatValue(floorInput.value);
|
|
238
|
+
applyWindow();
|
|
239
|
+
});
|
|
240
|
+
model.on("change:cal_max", () => {
|
|
241
|
+
if (ceilInput) ceilInput.value = model.get("cal_max") ?? ceilInput.value;
|
|
242
|
+
if (ceilValue) ceilValue.textContent = formatValue(ceilInput.value);
|
|
243
|
+
applyWindow();
|
|
244
|
+
});
|
|
245
|
+
model.on("change:slice_type", () =>
|
|
246
|
+
nv.setSliceType(SLICE_TYPE[model.get("slice_type")] ?? SLICE_TYPE.MULTIPLANAR),
|
|
247
|
+
);
|
|
248
|
+
model.on("change:colorbar", () => {
|
|
249
|
+
nv.opts.isColorbar = model.get("colorbar");
|
|
250
|
+
nv.drawScene();
|
|
251
|
+
});
|
|
252
|
+
model.on("change:atlas_outline", () => {
|
|
253
|
+
nv.opts.atlasOutline = model.get("atlas_outline") || 0;
|
|
254
|
+
nv.drawScene();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// niivue has no destroy(); drop the WebGL context on teardown / re-render
|
|
258
|
+
// so repeated cell runs don't leak GL contexts (browsers cap them).
|
|
259
|
+
return () => {
|
|
260
|
+
canvas
|
|
261
|
+
.getContext("webgl2")
|
|
262
|
+
?.getExtension("WEBGL_lose_context")
|
|
263
|
+
?.loseContext();
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
};
|