MNutils 1.2.0__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 (41) hide show
  1. mnutils/GEExam.py +495 -0
  2. mnutils/GESeries.py +1578 -0
  3. mnutils/__init__.py +54 -0
  4. mnutils/_scripts.py +177 -0
  5. mnutils/fitting/AMARES.py +831 -0
  6. mnutils/fitting/__init__.py +5 -0
  7. mnutils/matlab/__init__.py +13 -0
  8. mnutils/matlab/setup_matlab.py +116 -0
  9. mnutils/plotting/__init__.py +38 -0
  10. mnutils/plotting/_widgets/__init__.py +5 -0
  11. mnutils/plotting/_widgets/_html.py +34 -0
  12. mnutils/plotting/_widgets/_shared/__init__.py +50 -0
  13. mnutils/plotting/_widgets/_shared/dom.js +124 -0
  14. mnutils/plotting/_widgets/_shared/theme.css +140 -0
  15. mnutils/plotting/_widgets/image_grid.css +202 -0
  16. mnutils/plotting/_widgets/image_grid.js +212 -0
  17. mnutils/plotting/_widgets/image_grid.py +86 -0
  18. mnutils/plotting/_widgets/mrsi_inspector.css +376 -0
  19. mnutils/plotting/_widgets/mrsi_inspector.js +681 -0
  20. mnutils/plotting/_widgets/mrsi_inspector.py +70 -0
  21. mnutils/plotting/_widgets/slice_viewer.css +38 -0
  22. mnutils/plotting/_widgets/slice_viewer.js +41 -0
  23. mnutils/plotting/_widgets/slice_viewer.py +35 -0
  24. mnutils/plotting/images.py +1399 -0
  25. mnutils/plotting/saving.py +138 -0
  26. mnutils/plotting/spectra.py +357 -0
  27. mnutils/py.typed +0 -0
  28. mnutils/testing/__init__.py +3 -0
  29. mnutils/testing/_spectra.py +338 -0
  30. mnutils/testing/synthetic.py +373 -0
  31. mnutils/utils/__init__.py +25 -0
  32. mnutils/utils/data_loaders.py +218 -0
  33. mnutils/utils/file_helpers.py +708 -0
  34. mnutils/utils/images.py +202 -0
  35. mnutils/utils/nifti.py +379 -0
  36. mnutils/utils/spectra.py +149 -0
  37. mnutils-1.2.0.dist-info/METADATA +161 -0
  38. mnutils-1.2.0.dist-info/RECORD +41 -0
  39. mnutils-1.2.0.dist-info/WHEEL +4 -0
  40. mnutils-1.2.0.dist-info/entry_points.txt +7 -0
  41. mnutils-1.2.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,376 @@
1
+ .mnutils-mrsi-inspector {
2
+ /* Both panels give their middle element exactly this height, which is what
3
+ makes the anatomical frame and the spectrum plot line up top and bottom.
4
+ Clamped rather than fixed so it still adapts to short viewports, but never
5
+ grows into a full-screen-tall plot on a large one. */
6
+ --mnu-mrsi-media-h: clamp(200px, 40vh, 380px);
7
+
8
+ /* Every slider in the widget -- the native slice input and both nouislider
9
+ ones -- is drawn from these two, so the tracks stay the same weight and
10
+ the handle offsets below can be derived rather than hand-tuned per
11
+ orientation.
12
+
13
+ Sized in `em` so the controls follow the host's type rather than a fixed
14
+ pixel target: the same 22px thumb that reads as comfortable in a VS Code
15
+ notebook (13px base) is oversized on the docs site (16px+). The clamps keep
16
+ it a real pointing target at the small end and stop it turning into a knob
17
+ at the large one. */
18
+ --mnu-mrsi-track: clamp(5px, 0.4em, 8px);
19
+ --mnu-mrsi-thumb: clamp(13px, 1.15em, 18px);
20
+
21
+ flex-direction: row;
22
+ flex-wrap: wrap;
23
+ gap: 1em;
24
+ padding: 0.7em;
25
+ outline: none;
26
+ box-shadow: none;
27
+ border: none;
28
+ }
29
+
30
+ .mnutils-mrsi-left-panel,
31
+ .mnutils-mrsi-right-panel {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 0.4em;
35
+ min-width: 0;
36
+ }
37
+
38
+ /* One third image, two thirds spectrum: the anatomical frame is a square the
39
+ reader only needs big enough to aim at, while the spectrum is where the
40
+ detail is. `flex-basis: 0` makes the 1:2 grow ratio govern the split
41
+ outright, instead of being applied to whatever is left over after two
42
+ different content-derived basis widths. `flex-wrap` on the container still
43
+ stacks them at narrow widths, once min-width kicks in.
44
+
45
+ The two min-widths also decide *when* the panels stop sharing a row: each is
46
+ the narrowest its panel still reads at -- a spectrum needs far more room than
47
+ a head, hence the asymmetry. Set this way the layout stacks while there is
48
+ still space to spare, instead of squeezing both until the ppm handles merge
49
+ and the slice readout is pushed out of its panel. Below their sum the widget
50
+ is one column and every control gets the full width.
51
+
52
+ Wrapped in `min(..., 100%)` so the floor never exceeds what the widget
53
+ actually has: on a phone the 480px panel is wider than the screen, and a
54
+ plain min-width would push the ppm bar off the side instead of stacking. */
55
+ .mnutils-mrsi-left-panel {
56
+ flex: 1 1 0;
57
+ min-width: min(320px, 100%);
58
+ }
59
+
60
+ .mnutils-mrsi-right-panel {
61
+ flex: 2 1 0;
62
+ min-width: min(480px, 100%);
63
+ }
64
+
65
+ /* The fixed-height row both panels hang their main content in. The image is
66
+ centred inside it rather than stretched: the frame keeps its aspect ratio
67
+ and the leftover width stays empty, which is cheaper to read than a squashed
68
+ head. */
69
+ .mnutils-mrsi-media {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ gap: 0.4em;
74
+ height: var(--mnu-mrsi-media-h);
75
+ min-height: 0;
76
+ }
77
+
78
+ /* Sized by the frame's own aspect ratio (`--mnu-mrsi-ar`, set inline from the
79
+ payload's image_width/image_height -- square for the usual T1 grid), so the
80
+ image is never distorted: the box is as tall as the media row allows, unless
81
+ that would make it wider than the panel, in which case the width caps and
82
+ the height follows it down. Setting `height: 100%` and capping `max-width`
83
+ instead is what squashed the frame -- the width shrank and the height didn't.
84
+
85
+ The wrapper also has to be exactly the image box, because the SVG overlay is
86
+ positioned against it (inset: 0); any letterboxing between the two would
87
+ offset the voxel outline from the voxel it marks. */
88
+ .mnutils-mrsi-image-wrap {
89
+ --mnu-mrsi-ar: 1;
90
+
91
+ position: relative;
92
+ flex: 0 0 auto;
93
+ width: min(100%, calc(var(--mnu-mrsi-media-h) * var(--mnu-mrsi-ar)));
94
+ aspect-ratio: var(--mnu-mrsi-ar);
95
+ background: var(--mnu-panel);
96
+ border: 1px solid var(--mnu-border);
97
+ border-radius: 6px;
98
+ overflow: hidden;
99
+ box-sizing: border-box;
100
+ }
101
+
102
+ .mnutils-mrsi-image {
103
+ display: block;
104
+ width: 100%;
105
+ height: 100%;
106
+ cursor: crosshair;
107
+ }
108
+
109
+ /* Purely a drawing surface: the voxel outline sits on top of the frame, but
110
+ the click handler lives on the <img> underneath it. Without this the SVG --
111
+ a full-size absolutely positioned box, painted last -- wins every hit test
112
+ over the image and swallows the clicks the voxel picker exists to receive.
113
+ Inherited by the polygons, which have nothing to handle either. */
114
+ .mnutils-mrsi-overlay {
115
+ position: absolute;
116
+ inset: 0;
117
+ width: 100%;
118
+ height: 100%;
119
+ pointer-events: none;
120
+ }
121
+
122
+ /* Two stacked polygons, not one: over a T1 the box crosses both black
123
+ background and bright tissue, and a single accent stroke disappears into
124
+ whichever of the two happens to sit under it. The halo is drawn first
125
+ (wider, dark, semi-transparent) so the accent line always has contrast, and
126
+ a faint fill marks the selected cell without hiding the MRSI colour under
127
+ it. */
128
+ .mnutils-mrsi-voxel-box {
129
+ fill: color-mix(in srgb, var(--mnu-accent) 12%, transparent);
130
+ stroke: var(--mnu-accent);
131
+ stroke-width: 2;
132
+ stroke-linejoin: round;
133
+ vector-effect: non-scaling-stroke;
134
+ }
135
+
136
+ .mnutils-mrsi-voxel-box-halo {
137
+ fill: none;
138
+ stroke: rgba(0, 0, 0, 0.55);
139
+ stroke-width: 4;
140
+ stroke-linejoin: round;
141
+ vector-effect: non-scaling-stroke;
142
+ }
143
+
144
+ /* Fixed line box, not just centred text: the two titles are what sits above the
145
+ two media boxes, so they have to occupy the same height or the image and the
146
+ plot start at different y positions. */
147
+ .mnutils-mrsi-slice-title,
148
+ .mnutils-mrsi-spectrum-title {
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ text-align: center;
153
+ font-size: 1.05em;
154
+ min-height: 2.2em;
155
+ line-height: 1.25;
156
+ }
157
+
158
+ .mnutils-mrsi-spectrum-container {
159
+ position: relative;
160
+ flex: 1 1 auto;
161
+ min-width: 0;
162
+ height: 100%;
163
+ padding: 0.4em;
164
+ box-sizing: border-box;
165
+ background: var(--mnu-panel);
166
+ border: 1px solid var(--mnu-border);
167
+ border-radius: 6px;
168
+ }
169
+
170
+ .mnutils-mrsi-spectrum-container > canvas {
171
+ display: block;
172
+ }
173
+
174
+ /* Both control rows read as one component: a bordered strip under the media
175
+ box, rather than a bare slider floating on the widget background. */
176
+ .mnutils-mrsi-control-bar {
177
+ gap: 0.7em;
178
+ padding: 0.45em 0.7em;
179
+ background: var(--mnu-panel);
180
+ border: 1px solid var(--mnu-border);
181
+ border-radius: 6px;
182
+ }
183
+
184
+ .mnutils-mrsi-range-bar {
185
+ gap: 0.8em;
186
+ }
187
+
188
+ /* The shared bar text is sized for the slice viewer's compact strip; next to a
189
+ full-height plot and thumbs this size it reads as fine print. Bumped here
190
+ rather than in theme.css so the other widgets keep their own proportions.
191
+ The readout also gets more room -- it carries two signed ppm values. */
192
+ .mnutils-mrsi-inspector .mnu-lbl,
193
+ .mnutils-mrsi-inspector .mnu-readout {
194
+ font-size: 0.95em;
195
+ }
196
+
197
+ /* Never the part that gives way when the bar tightens: the readout is the only
198
+ place the numeric value appears, so it holds its content width and the track
199
+ absorbs the loss. */
200
+ .mnutils-mrsi-inspector .mnu-lbl {
201
+ flex: 0 0 auto;
202
+ }
203
+
204
+ .mnutils-mrsi-inspector .mnu-readout {
205
+ flex: 0 0 auto;
206
+ min-width: 5.5em;
207
+ white-space: nowrap;
208
+ }
209
+
210
+ /* Native range inputs are styled per-browser and next to nouislider's flat
211
+ track the default one looks like a different control; match the two.
212
+
213
+ `min-width: 0` is what keeps the bar intact when the panel narrows: a range
214
+ input carries an intrinsic width (~129px in Chromium) that `flex: 1` alone
215
+ will not shrink past, so the row grew wider than its panel and shoved the
216
+ readout out of sight rather than giving up track. */
217
+ .mnutils-mrsi-inspector .mnu-slider {
218
+ -webkit-appearance: none;
219
+ appearance: none;
220
+ min-width: 0;
221
+ height: var(--mnu-mrsi-track);
222
+ border-radius: 999px;
223
+ background: var(--mnu-border);
224
+ cursor: pointer;
225
+ }
226
+
227
+ .mnutils-mrsi-inspector .mnu-slider:disabled {
228
+ opacity: 0.5;
229
+ cursor: default;
230
+ }
231
+
232
+ .mnutils-mrsi-inspector .mnu-slider::-webkit-slider-thumb {
233
+ -webkit-appearance: none;
234
+ appearance: none;
235
+ width: var(--mnu-mrsi-thumb);
236
+ height: var(--mnu-mrsi-thumb);
237
+ border: 1px solid var(--mnu-border);
238
+ border-radius: 50%;
239
+ background: var(--mnu-accent);
240
+ cursor: grab;
241
+ }
242
+
243
+ .mnutils-mrsi-inspector .mnu-slider::-moz-range-thumb {
244
+ width: var(--mnu-mrsi-thumb);
245
+ height: var(--mnu-mrsi-thumb);
246
+ border: 1px solid var(--mnu-border);
247
+ border-radius: 50%;
248
+ background: var(--mnu-accent);
249
+ cursor: grab;
250
+ }
251
+
252
+ /* Sits under the ppm bar as a secondary control, so keep it visually
253
+ subordinate to it: a self-shrinking pill rather than a full-width row, muted
254
+ until hovered or checked. */
255
+ .mnutils-mrsi-ylim-toggle {
256
+ display: inline-flex;
257
+ align-self: center;
258
+ align-items: center;
259
+ gap: 0.45em;
260
+ margin-top: 0.1em;
261
+ padding: 0.35em 0.8em;
262
+ border: 1px solid var(--mnu-border);
263
+ border-radius: 999px;
264
+ background: var(--mnu-panel);
265
+ font-size: 0.95em;
266
+ font-weight: 600;
267
+ color: var(--mnu-label);
268
+ line-height: 1.4;
269
+ opacity: 0.75;
270
+ cursor: pointer;
271
+ user-select: none;
272
+ transition:
273
+ opacity 0.12s ease,
274
+ border-color 0.12s ease;
275
+ }
276
+
277
+ .mnutils-mrsi-ylim-toggle:hover,
278
+ .mnutils-mrsi-ylim-toggle:focus-within {
279
+ opacity: 1;
280
+ }
281
+
282
+ .mnutils-mrsi-ylim-toggle:has(input:checked) {
283
+ opacity: 1;
284
+ border-color: var(--mnu-accent);
285
+ }
286
+
287
+ .mnutils-mrsi-ylim-toggle input {
288
+ width: 1.1em;
289
+ height: 1.1em;
290
+ accent-color: var(--mnu-accent);
291
+ cursor: pointer;
292
+ margin: 0;
293
+ }
294
+
295
+ /* Theme nouislider (the ppm dual-range and the y-limit one) to match the rest
296
+ of the widget's accent/border tokens instead of its default blue skin. */
297
+ .mnutils-mrsi-slider.noUi-target {
298
+ background: var(--mnu-border);
299
+ border: none;
300
+ box-shadow: none;
301
+ }
302
+
303
+ .mnutils-mrsi-slider .noUi-connect {
304
+ background: var(--mnu-accent);
305
+ }
306
+
307
+ .mnutils-mrsi-slider .noUi-handle {
308
+ width: var(--mnu-mrsi-thumb);
309
+ height: var(--mnu-mrsi-thumb);
310
+ border-color: var(--mnu-border);
311
+ border-radius: 50%;
312
+ box-shadow: none;
313
+ background: var(--mnu-accent);
314
+ cursor: grab;
315
+ }
316
+
317
+ /* Centre the handle on the track, whichever way the track runs: the cross-axis
318
+ offset is half the difference between the two, the along-axis one half a
319
+ handle. Both derive from the size tokens, so resizing the target needs no
320
+ re-tuning here. nouislider stamps the orientation class on the target itself.
321
+
322
+ The two orientations anchor the handle to opposite edges of its origin
323
+ element -- `right` + `top` horizontally, `right` + *`bottom`* vertically --
324
+ because a vertical origin marks its value at its bottom edge. Overriding
325
+ `top` on a vertical handle therefore doesn't nudge it, it re-anchors it to
326
+ the far end of the track: both handles ended up a full track-length from the
327
+ values they stood for, off the side of the panel. Reset the edge the
328
+ stylesheet used, don't just add to it. */
329
+ .mnutils-mrsi-slider.noUi-horizontal .noUi-handle {
330
+ top: calc((var(--mnu-mrsi-track) - var(--mnu-mrsi-thumb)) / 2);
331
+ bottom: auto;
332
+ right: calc(var(--mnu-mrsi-thumb) / -2);
333
+ }
334
+
335
+ .mnutils-mrsi-slider.noUi-vertical .noUi-handle {
336
+ top: auto;
337
+ bottom: calc(var(--mnu-mrsi-thumb) / -2);
338
+ right: calc((var(--mnu-mrsi-track) - var(--mnu-mrsi-thumb)) / 2);
339
+ }
340
+
341
+ .mnutils-mrsi-slider .noUi-handle:focus-visible {
342
+ outline: 2px solid var(--mnu-focus-ring);
343
+ outline-offset: 2px;
344
+ }
345
+
346
+ .mnutils-mrsi-slider .noUi-active {
347
+ background: var(--mnu-accent-strong);
348
+ cursor: grabbing;
349
+ }
350
+
351
+ .mnutils-mrsi-slider .noUi-handle::before,
352
+ .mnutils-mrsi-slider .noUi-handle::after {
353
+ display: none;
354
+ }
355
+
356
+ .mnutils-mrsi-ppm-slider {
357
+ flex: 1;
358
+ height: var(--mnu-mrsi-track);
359
+ margin: 0 0.6em;
360
+ }
361
+
362
+ /* Sits inside the media row, left of the chart, and is sized off the row's own
363
+ height rather than stretched: nouislider needs a resolved height at create
364
+ time, and the 3em trimmed off the bottom is roughly the strip Chart.js gives
365
+ the x axis and its title, which keeps the track spanning the plot area
366
+ rather than the whole canvas. */
367
+ .mnutils-mrsi-slider.mnutils-mrsi-ylim-slider {
368
+ flex: 0 0 auto;
369
+ width: var(--mnu-mrsi-track);
370
+ height: calc(var(--mnu-mrsi-media-h) - 3em);
371
+ align-self: flex-start;
372
+ /* Room for the thumb, which overhangs the track by half its width on every
373
+ side: without it the handles graze the plot panel and the topmost one
374
+ clips out of the media row. */
375
+ margin: 0.9em 0.5em 0 0.4em;
376
+ }