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.
Files changed (95) hide show
  1. nltools/__init__.py +55 -0
  2. nltools/algorithms/__init__.py +90 -0
  3. nltools/algorithms/alignment/__init__.py +21 -0
  4. nltools/algorithms/alignment/procrustes.py +565 -0
  5. nltools/algorithms/alignment/srm.py +758 -0
  6. nltools/algorithms/backends.py +1059 -0
  7. nltools/algorithms/corrections.py +177 -0
  8. nltools/algorithms/decoding.py +327 -0
  9. nltools/algorithms/inference/__init__.py +50 -0
  10. nltools/algorithms/inference/bootstrap.py +1386 -0
  11. nltools/algorithms/inference/correlation.py +373 -0
  12. nltools/algorithms/inference/intersubject.py +422 -0
  13. nltools/algorithms/inference/isc.py +1554 -0
  14. nltools/algorithms/inference/matrix.py +602 -0
  15. nltools/algorithms/inference/one_sample.py +288 -0
  16. nltools/algorithms/inference/random.py +122 -0
  17. nltools/algorithms/inference/timeseries.py +347 -0
  18. nltools/algorithms/inference/two_sample.py +212 -0
  19. nltools/algorithms/inference/utils.py +58 -0
  20. nltools/algorithms/inference/validation.py +282 -0
  21. nltools/algorithms/neighborhoods.py +207 -0
  22. nltools/algorithms/outliers.py +308 -0
  23. nltools/algorithms/regression.py +83 -0
  24. nltools/algorithms/signal.py +303 -0
  25. nltools/algorithms/similarity.py +234 -0
  26. nltools/algorithms/validation.py +151 -0
  27. nltools/cross_validation.py +72 -0
  28. nltools/data/__init__.py +30 -0
  29. nltools/data/adjacency/__init__.py +875 -0
  30. nltools/data/adjacency/io.py +111 -0
  31. nltools/data/adjacency/modeling.py +569 -0
  32. nltools/data/adjacency/plotting.py +174 -0
  33. nltools/data/adjacency/state.py +349 -0
  34. nltools/data/adjacency/stats.py +596 -0
  35. nltools/data/adjacency/utils.py +79 -0
  36. nltools/data/atlases/__init__.py +23 -0
  37. nltools/data/atlases/labeling.py +158 -0
  38. nltools/data/atlases/loading.py +76 -0
  39. nltools/data/atlases/registry.py +96 -0
  40. nltools/data/atlases/reporting.py +456 -0
  41. nltools/data/braindata/__init__.py +2170 -0
  42. nltools/data/braindata/analysis.py +1381 -0
  43. nltools/data/braindata/bootstrap.py +398 -0
  44. nltools/data/braindata/io.py +896 -0
  45. nltools/data/braindata/modeling.py +594 -0
  46. nltools/data/braindata/plotting.py +501 -0
  47. nltools/data/braindata/prediction.py +1250 -0
  48. nltools/data/braindata/utils.py +348 -0
  49. nltools/data/braindata/validation.py +197 -0
  50. nltools/data/braindata/viewer.js +266 -0
  51. nltools/data/braindata/viewer.py +770 -0
  52. nltools/data/combine.py +27 -0
  53. nltools/data/designmatrix/__init__.py +1032 -0
  54. nltools/data/designmatrix/append.py +518 -0
  55. nltools/data/designmatrix/diagnostics.py +248 -0
  56. nltools/data/designmatrix/io.py +356 -0
  57. nltools/data/designmatrix/plotting.py +291 -0
  58. nltools/data/designmatrix/regressors.py +463 -0
  59. nltools/data/designmatrix/transforms.py +200 -0
  60. nltools/data/designmatrix/utils.py +350 -0
  61. nltools/data/ownership.py +129 -0
  62. nltools/data/results.py +291 -0
  63. nltools/data/roc/__init__.py +398 -0
  64. nltools/data/simulator/__init__.py +927 -0
  65. nltools/data/simulator/haxby.py +124 -0
  66. nltools/data/validation.py +83 -0
  67. nltools/datasets.py +218 -0
  68. nltools/io/__init__.py +10 -0
  69. nltools/io/events.py +67 -0
  70. nltools/io/h5.py +246 -0
  71. nltools/mask.py +403 -0
  72. nltools/models/__init__.py +11 -0
  73. nltools/models/glm.py +543 -0
  74. nltools/models/results.py +49 -0
  75. nltools/models/ridge.py +1303 -0
  76. nltools/models/validation.py +26 -0
  77. nltools/plotting/__init__.py +32 -0
  78. nltools/plotting/adjacency.py +421 -0
  79. nltools/plotting/brain.py +669 -0
  80. nltools/plotting/decomposition.py +111 -0
  81. nltools/plotting/prediction.py +110 -0
  82. nltools/resources/covariates_example.csv +161 -0
  83. nltools/resources/onsets_example.csv +40 -0
  84. nltools/templates/__init__.py +51 -0
  85. nltools/templates/config.py +144 -0
  86. nltools/templates/fetch.py +260 -0
  87. nltools/templates/matching.py +183 -0
  88. nltools/templates/paths.py +106 -0
  89. nltools/templates/registry.py +25 -0
  90. nltools/utils.py +230 -0
  91. nltools/version.py +13 -0
  92. nltools-0.6.0.dev0.dist-info/METADATA +95 -0
  93. nltools-0.6.0.dev0.dist-info/RECORD +95 -0
  94. nltools-0.6.0.dev0.dist-info/WHEEL +4 -0
  95. 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
+ };