PySourceviz 0.1.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.
@@ -0,0 +1,849 @@
1
+ Metadata-Version: 2.4
2
+ Name: PySourceviz
3
+ Version: 0.1.0
4
+ Summary: Publication-ready static cortical source-estimate figures
5
+ Author-email: LiaoPan <LiaoPan_2015@163.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/LiaoPan/PySourceviz
8
+ Project-URL: Repository, https://github.com/LiaoPan/PySourceviz
9
+ Project-URL: Issues, https://github.com/LiaoPan/PySourceviz/issues
10
+ Keywords: MNE-Python,MEG,EEG,source localization,cortical surface,BrainSpace,neuroimaging,visualization
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Classifier: Topic :: Scientific/Engineering :: Visualization
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE.txt
23
+ Requires-Dist: mne>=1.7
24
+ Requires-Dist: brainspace>=0.2
25
+ Requires-Dist: numpy>=1.23
26
+ Requires-Dist: nibabel>=5.0
27
+ Provides-Extra: nilearn
28
+ Requires-Dist: nilearn>=0.12; extra == "nilearn"
29
+ Provides-Extra: comparison
30
+ Requires-Dist: nilearn>=0.12; extra == "comparison"
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.2; extra == "dev"
33
+ Requires-Dist: twine>=5; extra == "dev"
34
+ Requires-Dist: tomli>=2; python_version < "3.11" and extra == "dev"
35
+ Requires-Dist: pillow>=8; extra == "dev"
36
+ Requires-Dist: pyyaml>=6; extra == "dev"
37
+ Requires-Dist: pytest>=7.4; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # PySourceviz
41
+
42
+ **English** | [简体中文](https://github.com/LiaoPan/PySourceviz/blob/main/README.zh-CN.md)
43
+
44
+ [![Tests](https://github.com/LiaoPan/PySourceviz/actions/workflows/tests.yml/badge.svg)](https://github.com/LiaoPan/PySourceviz/actions/workflows/tests.yml)
45
+ [![Quality gates](https://github.com/LiaoPan/PySourceviz/actions/workflows/quality-gates.yml/badge.svg)](https://github.com/LiaoPan/PySourceviz/actions/workflows/quality-gates.yml)
46
+
47
+ PySourceviz turns MNE-Python cortical source estimates into consistent,
48
+ publication-friendly surface figures with a selectable BrainSpace or Nilearn
49
+ renderer. It keeps source-estimate semantics in MNE, uses FreeSurfer anatomy
50
+ for the mesh and medial wall, and renders four comparable views with one
51
+ whole-brain activation colorbar.
52
+
53
+ The v0.1 visual defaults are an inflated cortical surface, white background,
54
+ soft white-to-light-gray sulcal shading, a transparent low-value overlay, and
55
+ this anatomical order:
56
+
57
+ ```text
58
+ LH lateral | LH medial | RH medial | RH lateral
59
+ ```
60
+
61
+ For source-localization overview figures, PySourceviz recommends the inflated
62
+ surface used by default in
63
+ [`mne.SourceEstimate.plot`](https://mne.tools/stable/generated/mne.SourceEstimate.html#mne.SourceEstimate.plot).
64
+ Inflation exposes activation that would be hidden inside sulci and avoids
65
+ visually equating detailed fold boundaries with the spatial resolution of the
66
+ inverse solution. It changes only display coordinates: source vertices,
67
+ activation values, timing, interpolation, thresholds, and color limits remain
68
+ unchanged. `pial` and `white` remain available when their anatomy is the
69
+ intended subject of the figure.
70
+
71
+ ## Result gallery
72
+
73
+ **PySourceviz · MNE audvis dSPM · inflated surface · 110 ms actual sample**
74
+
75
+ ![PySourceviz MNE audvis source estimate at a 112 ms request](https://raw.githubusercontent.com/LiaoPan/PySourceviz/main/assets/pysourceviz-sample-112ms.png)
76
+
77
+ The requested time above is 112 ms. The STC is sampled every 10 ms, so
78
+ PySourceviz selects 110 ms and records that actual sample in the returned
79
+ metadata.
80
+
81
+ **PySourceviz · BrainSpace backend · inflated surface · p98-p99.9 color controls**
82
+
83
+ ![BrainSpace renderer using the shared audvis activation](https://raw.githubusercontent.com/LiaoPan/PySourceviz/main/assets/nilearn-comparison/brainspace.png)
84
+
85
+ **PySourceviz · Nilearn backend · inflated surface · the same activation and limits**
86
+
87
+ ![Nilearn renderer using the same audvis activation](https://raw.githubusercontent.com/LiaoPan/PySourceviz/main/assets/nilearn-comparison/nilearn.png)
88
+
89
+ Both images above are produced by built-in PySourceviz backends. They use the
90
+ same STC and time sample. They also use the same inflated geometry, expanded
91
+ vertex values, medial-wall mask, FreeSurfer `sulc` background, colormap, and
92
+ color limits; only the final renderer changes. The white-gray texture is
93
+ therefore retained on the inflated coordinates rather than being tied to pial
94
+ geometry.
95
+
96
+ **PySourceviz · BrainSpace · pial surface · optional anatomical view**
97
+
98
+ ![Optional BrainSpace pial-surface rendering](https://raw.githubusercontent.com/LiaoPan/PySourceviz/main/assets/nilearn-comparison/brainspace-pial.png)
99
+
100
+ This pial view is included as an optional anatomical presentation.
101
+ It is not the recommended default for source-localization overviews. Use it
102
+ when the relationship to native gyral and sulcal folding is itself important.
103
+ The source estimate, selected 110 ms sample, interpolation, threshold, and
104
+ p98-p99.9 color controls are unchanged; only the display geometry differs.
105
+
106
+ ## Quickstart
107
+
108
+ Install PySourceviz with its default BrainSpace backend from PyPI after the
109
+ release is published:
110
+
111
+ ```bash
112
+ python -m pip install PySourceviz
113
+ ```
114
+
115
+ Install the optional Nilearn backend when you want Matplotlib-based surface
116
+ figures as well:
117
+
118
+ ```bash
119
+ python -m pip install "PySourceviz[nilearn]"
120
+ ```
121
+
122
+ Load an MNE surface STC, point `subjects_dir` to the directory that contains
123
+ the matching FreeSurfer subject, and save a PNG:
124
+
125
+ ```python
126
+ from pathlib import Path
127
+
128
+ import mne
129
+
130
+ from pysourceviz import plot_source
131
+
132
+
133
+ stc = mne.read_source_estimate("/data/inverse/audvis")
134
+ subjects_dir = Path("/data/freesurfer/subjects")
135
+
136
+ info = plot_source(
137
+ stc,
138
+ subjects_dir=subjects_dir,
139
+ backend="brainspace",
140
+ template="native",
141
+ time=0.112,
142
+ units="dSPM",
143
+ title="Auditory response · 112 ms request",
144
+ output="figures/audvis_112ms.png",
145
+ )
146
+
147
+ print(info["selected_time"]) # actual nearest STC sample, for example 0.11
148
+ print(info["color_range"]) # shared whole-brain display range
149
+ print(info["output"]) # resolved PNG path
150
+ ```
151
+
152
+ Render the same prepared source map with Nilearn by changing one argument:
153
+
154
+ ```python
155
+ nilearn_info = plot_source(
156
+ stc,
157
+ subjects_dir=subjects_dir,
158
+ backend="nilearn",
159
+ template="native",
160
+ time=0.112,
161
+ clim={"kind": "percent", "lims": [98, 99, 99.9]},
162
+ units="dSPM",
163
+ title="Auditory response · Nilearn",
164
+ output="figures/audvis_112ms_nilearn.png",
165
+ )
166
+ ```
167
+
168
+ Time selection, interpolation, threshold, `clim`, colormap, layout, output
169
+ size, and returned scientific metadata have the same meaning for both
170
+ backends. Nilearn is imported only when `backend="nilearn"` is selected.
171
+
172
+ For `stc.subject == "fsaverage"`, the example expects files such as
173
+ `/data/freesurfer/subjects/fsaverage/surf/lh.inflated`. `subjects_dir` is one
174
+ level above the subject; it is not the `fsaverage` directory itself.
175
+
176
+ ## Complete usage reference
177
+
178
+ The recipes below enumerate every supported source family and public control.
179
+ All time values are seconds. PySourceviz never modifies the input STC.
180
+
181
+ ### Supported source-estimate families
182
+
183
+ | MNE object | PySourceviz v0.1 behavior |
184
+ | --- | --- |
185
+ | `mne.SourceEstimate` | Real scalar cortical data; supports `mode="magnitude"` and `mode="signed"`. |
186
+ | `mne.VectorSourceEstimate` | Cortical vector magnitude; signed display is not defined. |
187
+ | `mne.MixedSourceEstimate` | Uses only the cortical surface component. |
188
+ | `mne.MixedVectorSourceEstimate` | Uses the cortical surface component and converts vectors to magnitude. |
189
+ | `mne.VolSourceEstimate` / `mne.VolVectorSourceEstimate` | Unsupported volume-only data; use a volume plotting workflow instead. |
190
+
191
+ Complex STCs are unsupported. PySourceviz also does not compute inverse
192
+ solutions or silently turn volumetric results into cortical surface data.
193
+
194
+ ### Choose BrainSpace or Nilearn
195
+
196
+ BrainSpace is the backward-compatible default and uses VTK. Nilearn is an
197
+ optional Matplotlib-based renderer:
198
+
199
+ ```python
200
+ plot_source(
201
+ stc,
202
+ subjects_dir=subjects_dir,
203
+ backend="brainspace",
204
+ time=0.112,
205
+ output="brainspace.png",
206
+ )
207
+
208
+ plot_source(
209
+ stc,
210
+ subjects_dir=subjects_dir,
211
+ backend="nilearn",
212
+ time=0.112,
213
+ output="nilearn.png",
214
+ )
215
+ ```
216
+
217
+ Both calls pass the same prepared vertex arrays to the selected renderer. A
218
+ static-only save returns `info["plotter"] is None`. Without a static-only save,
219
+ that field contains a BrainSpace plotter or a Matplotlib `Figure`, respectively.
220
+ Call `.close()` on a retained BrainSpace plotter or
221
+ `matplotlib.pyplot.close(info["plotter"])` on a retained Nilearn figure when it
222
+ is no longer needed.
223
+
224
+ ### Select a time or reduce a time window
225
+
226
+ `time=None` is valid for an STC that already has exactly one sample:
227
+
228
+ ```python
229
+ plot_source(single_time_stc, subjects_dir=subjects_dir, time=None)
230
+ ```
231
+
232
+ A numeric request chooses the nearest actual sample. `time="peak"` chooses the
233
+ sample containing the largest whole-brain magnitude:
234
+
235
+ ```python
236
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112)
237
+ plot_source(stc, subjects_dir=subjects_dir, time="peak")
238
+ ```
239
+
240
+ For an inclusive window, use either the signed/absolute mean or non-negative
241
+ RMS reduction:
242
+
243
+ ```python
244
+ plot_source(
245
+ stc,
246
+ subjects_dir=subjects_dir,
247
+ time_window=(0.080, 0.160),
248
+ reduce="mean",
249
+ )
250
+
251
+ plot_source(
252
+ stc,
253
+ subjects_dir=subjects_dir,
254
+ time_window=(0.080, 0.160),
255
+ reduce="rms",
256
+ mode="magnitude",
257
+ )
258
+ ```
259
+
260
+ `time` and `time_window` are mutually exclusive. Signed RMS is rejected because
261
+ RMS has no sign.
262
+
263
+ ### Choose native, fsaverage, or another matching subject
264
+
265
+ `template="native"` resolves to `stc.subject`. An explicit subject name must
266
+ match `stc.subject`:
267
+
268
+ ```python
269
+ plot_source(
270
+ stc_native,
271
+ subjects_dir=subjects_dir,
272
+ template="native",
273
+ time=0.112,
274
+ )
275
+
276
+ plot_source(
277
+ stc_fsaverage,
278
+ subjects_dir=subjects_dir,
279
+ template="fsaverage",
280
+ time=0.112,
281
+ )
282
+
283
+ plot_source(
284
+ stc_study_average,
285
+ subjects_dir=subjects_dir,
286
+ template="my_study_average",
287
+ time=0.112,
288
+ )
289
+ ```
290
+
291
+ Cross-subject registration is deliberately external. Morph first with MNE,
292
+ then plot the already matching result:
293
+
294
+ ```python
295
+ import mne
296
+
297
+
298
+ morph = mne.compute_source_morph(
299
+ stc_native,
300
+ subject_from=stc_native.subject,
301
+ subject_to="fsaverage",
302
+ subjects_dir=subjects_dir,
303
+ )
304
+ stc_fsaverage = morph.apply(stc_native)
305
+
306
+ plot_source(
307
+ stc_fsaverage,
308
+ subjects_dir=subjects_dir,
309
+ template="fsaverage",
310
+ time=0.112,
311
+ output="subject_on_fsaverage.png",
312
+ )
313
+ ```
314
+
315
+ ### Choose cortical anatomy and display interpolation
316
+
317
+ All three supported FreeSurfer surfaces are explicit:
318
+
319
+ ```python
320
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, surface="inflated")
321
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, surface="pial")
322
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, surface="white")
323
+ ```
324
+
325
+ `inflated` is the recommended default for source-localization overview
326
+ figures. It unfolds buried cortex without changing the data assigned to each
327
+ vertex. Use `pial` when the relationship to native cortical folding is itself
328
+ important, and `white` when the gray-white boundary is the intended anatomy;
329
+ neither alternative changes the inverse solution.
330
+
331
+ Sparse source vertices can be expanded to the display mesh in three ways:
332
+
333
+ ```python
334
+ # Recommended default: let MNE propagate until the surface is covered.
335
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, smoothing_steps=None)
336
+
337
+ # Legacy piecewise-constant nearest-source assignment.
338
+ plot_source(
339
+ stc,
340
+ subjects_dir=subjects_dir,
341
+ time=0.112,
342
+ smoothing_steps="nearest",
343
+ )
344
+
345
+ # Exact non-negative number of adjacency-propagation iterations.
346
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, smoothing_steps=5)
347
+ ```
348
+
349
+ This is same-subject display interpolation, not Gaussian or statistical
350
+ smoothing. Thresholds and color limits are computed from original sparse
351
+ values before display expansion.
352
+
353
+ ### Choose magnitude or signed color semantics
354
+
355
+ Magnitude is non-negative and defaults to the sequential `Reds` colormap:
356
+
357
+ ```python
358
+ plot_source(
359
+ stc,
360
+ subjects_dir=subjects_dir,
361
+ time=0.112,
362
+ mode="magnitude",
363
+ cmap="Reds",
364
+ units="dSPM",
365
+ )
366
+ ```
367
+
368
+ Signed scalar data default to `RdBu_r` and a symmetric range:
369
+
370
+ ```python
371
+ plot_source(
372
+ stc_scalar,
373
+ subjects_dir=subjects_dir,
374
+ time_window=(0.080, 0.160),
375
+ reduce="mean",
376
+ mode="signed",
377
+ output="signed_mean.png",
378
+ )
379
+ ```
380
+
381
+ Signed mode is unavailable for vector and mixed-vector STCs.
382
+
383
+ ### Choose a threshold and color limits
384
+
385
+ Leave the hard display mask disabled, use a numerical cutoff, or resolve a
386
+ percentile from the combined sparse hemispheres:
387
+
388
+ ```python
389
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, threshold=None)
390
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, threshold=3.0)
391
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, threshold="95%")
392
+ ```
393
+
394
+ Color limits support a robust percentile, an explicit range, and MNE-style
395
+ three-control dictionaries:
396
+
397
+ ```python
398
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, clim="robust")
399
+
400
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, clim=(0.0, 12.5))
401
+
402
+ plot_source(
403
+ stc,
404
+ subjects_dir=subjects_dir,
405
+ time=0.112,
406
+ clim={"kind": "percent", "lims": [98, 99, 99.9]},
407
+ )
408
+
409
+ plot_source(
410
+ stc,
411
+ subjects_dir=subjects_dir,
412
+ time=0.112,
413
+ clim={"kind": "value", "lims": [3.0, 4.0, 6.0]},
414
+ )
415
+
416
+ plot_source(
417
+ stc_signed,
418
+ subjects_dir=subjects_dir,
419
+ time=0.112,
420
+ mode="signed",
421
+ clim={"kind": "percent", "pos_lims": [95, 99, 99.9]},
422
+ )
423
+ ```
424
+
425
+ `robust_percentile=98.0` changes the percentile used by `clim="robust"`. For
426
+ dictionary clim, the lower, middle, and upper controls set the start of the
427
+ visibility ramp, the middle palette color/full overlay opacity, and the
428
+ saturation point. These controls and thresholds are visualization choices,
429
+ not tests of statistical significance.
430
+
431
+ ### Choose layout, styling, and output behavior
432
+
433
+ Use a four-panel row or a two-by-two grid, and optionally control the rest of
434
+ the presentation:
435
+
436
+ ```python
437
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, layout="row")
438
+ plot_source(stc, subjects_dir=subjects_dir, time=0.112, layout="grid")
439
+
440
+ plot_source(
441
+ stc,
442
+ subjects_dir=subjects_dir,
443
+ time=0.112,
444
+ title="Auditory response",
445
+ colorbar=False,
446
+ background="white",
447
+ size=(1600, 420),
448
+ scale=(2, 2),
449
+ output="auditory.png",
450
+ )
451
+
452
+ plot_source(
453
+ stc,
454
+ subjects_dir=subjects_dir,
455
+ time=0.112,
456
+ background=(1.0, 1.0, 1.0),
457
+ transparent=True,
458
+ output="auditory_transparent.png",
459
+ )
460
+ ```
461
+
462
+ The output/display combinations are:
463
+
464
+ ```python
465
+ # Save a PNG and close the static renderer (show defaults to False here).
466
+ saved = plot_source(
467
+ stc,
468
+ subjects_dir=subjects_dir,
469
+ time=0.112,
470
+ output="source.png",
471
+ )
472
+
473
+ # Open an interactive window (show defaults to True with no output).
474
+ interactive = plot_source(stc, subjects_dir=subjects_dir, time=0.112, show=True)
475
+
476
+ # Save and also create a separate interactive scene.
477
+ both = plot_source(
478
+ stc,
479
+ subjects_dir=subjects_dir,
480
+ time=0.112,
481
+ output="source_and_window.png",
482
+ show=True,
483
+ )
484
+
485
+ # Retain an offscreen plotter without saving or opening a window.
486
+ offscreen = plot_source(stc, subjects_dir=subjects_dir, time=0.112, show=False)
487
+ offscreen["plotter"].close()
488
+ ```
489
+
490
+ `output` accepts PNG paths only and creates missing parent directories. The
491
+ default logical size is 1600 x 420 for `layout="row"` and 900 x 760 for
492
+ `layout="grid"`; screenshot `scale=(2, 2)` doubles both pixel axes.
493
+
494
+ ### Compare conditions with one shared scale
495
+
496
+ Use the same preregistered or pooled range for every condition instead of a
497
+ separate robust range per image:
498
+
499
+ ```python
500
+ shared_clim = (0.0, 12.5)
501
+
502
+ info_a = plot_source(
503
+ stc_condition_a,
504
+ subjects_dir=subjects_dir,
505
+ time=0.112,
506
+ clim=shared_clim,
507
+ title="Condition A",
508
+ output="condition_a.png",
509
+ )
510
+ info_b = plot_source(
511
+ stc_condition_b,
512
+ subjects_dir=subjects_dir,
513
+ time=0.112,
514
+ clim=(0.0, 12.5),
515
+ title="Condition B",
516
+ output="condition_b.png",
517
+ )
518
+
519
+ assert info_a["color_range"] == info_b["color_range"] == shared_clim
520
+ ```
521
+
522
+ ### Inspect returned metadata
523
+
524
+ Every call returns a dictionary containing the resolved template and surface,
525
+ interpolation setting, time/window selection, threshold, whole-brain color
526
+ range and control points, colormap, layout, logical/pixel size controls, output
527
+ path, and `plotter`. Useful checks include:
528
+
529
+ ```python
530
+ for key in (
531
+ "backend",
532
+ "template",
533
+ "surface",
534
+ "smoothing_steps",
535
+ "selected_time",
536
+ "sampled_time_window",
537
+ "threshold",
538
+ "color_range",
539
+ "color_control_points",
540
+ "output",
541
+ ):
542
+ print(key, info[key])
543
+ ```
544
+
545
+ ### Run the bundled examples
546
+
547
+ | Program | Purpose |
548
+ | --- | --- |
549
+ | `examples/basic_usage.py` | Reusable BrainSpace/Nilearn fixed-time, window-RMS, and shared-clim helpers. |
550
+ | `examples/real_data_validation.py` | Five-image acceptance set plus a JSON provenance manifest. |
551
+ | `examples/compare_nilearn_backend.py` | Controlled BrainSpace/Nilearn comparison from one surface STC. |
552
+
553
+ ```bash
554
+ python examples/basic_usage.py
555
+
556
+ python examples/real_data_validation.py \
557
+ --stc-base /data/fsaverage_audvis_trunc-meg \
558
+ --subjects-dir /data/subjects \
559
+ --subject fsaverage \
560
+ --output-dir validation-output
561
+
562
+ python examples/compare_nilearn_backend.py \
563
+ --stc-base /data/fsaverage_audvis_trunc-meg \
564
+ --subjects-dir /data/subjects \
565
+ --subject fsaverage \
566
+ --output-dir renderer-comparison
567
+ ```
568
+
569
+ ## Complete `plot_source()` parameter reference
570
+
571
+ The table is synchronized with the public function signature. “Required” means
572
+ there is no default; all arguments after `stc` are keyword-only.
573
+
574
+ | Parameter | Default | Accepted values and effect |
575
+ | --- | --- | --- |
576
+ | `stc` | required | Real `mne.SourceEstimate`, vector surface STC, or mixed STC with a cortical component. |
577
+ | `subjects_dir` | required | Path containing the matching FreeSurfer subject directory. |
578
+ | `backend` | `"brainspace"` | `"brainspace"` or optional `"nilearn"`; scientific preprocessing is shared. |
579
+ | `template` | `"native"` | `"native"` or an explicit subject name equal to `stc.subject`. |
580
+ | `surface` | `"inflated"` | `"inflated"`, `"pial"`, or `"white"`. |
581
+ | `smoothing_steps` | `None` | Automatic MNE coverage, `"nearest"`, or a non-negative integer. |
582
+ | `time` | `None` | Seconds, `"peak"`, or `None`; mutually exclusive with `time_window`. |
583
+ | `time_window` | `None` | Inclusive `(start, stop)` seconds or `None`. |
584
+ | `reduce` | `"mean"` | `"mean"` or `"rms"` for a time window. |
585
+ | `mode` | `"magnitude"` | `"magnitude"` or scalar `"signed"`. |
586
+ | `threshold` | `None` | Non-negative number, percentile string such as `"95%"`, or `None`. |
587
+ | `clim` | `"robust"` | `"robust"`, `(lower, upper)`, or MNE-style three-control dictionary. |
588
+ | `robust_percentile` | `98.0` | Percentile in `(0, 100]` used by robust limits. |
589
+ | `cmap` | `None` | Matplotlib colormap name or mode-aware default. |
590
+ | `units` | `None` | Optional activation colorbar title. |
591
+ | `layout` | `"row"` | `"row"` or `"grid"`. |
592
+ | `title` | `None` | Optional figure title. |
593
+ | `colorbar` | `True` | Show or hide the shared activation colorbar. |
594
+ | `background` | `"white"` | Matplotlib color name or RGB triple. |
595
+ | `output` | `None` | Optional `.png` path; missing parent directories are created. |
596
+ | `size` | `None` | Logical `(width, height)` or the layout-specific default. |
597
+ | `scale` | `(2, 2)` | Positive integer screenshot scale for each pixel axis. |
598
+ | `transparent` | `False` | Use an alpha background for saved PNG output. |
599
+ | `show` | `None` | `True`, `False`, or output-aware default behavior. |
600
+
601
+ ## FreeSurfer `subjects_dir` in detail
602
+
603
+ For `subject="sample"` and `surface="inflated"`, PySourceviz reads:
604
+
605
+ ```text
606
+ <subjects_dir>/sample/surf/lh.inflated
607
+ <subjects_dir>/sample/surf/rh.inflated
608
+ <subjects_dir>/sample/surf/lh.sulc
609
+ <subjects_dir>/sample/surf/rh.sulc
610
+ <subjects_dir>/sample/label/lh.cortex.label
611
+ <subjects_dir>/sample/label/rh.cortex.label
612
+ ```
613
+
614
+ Missing surface or sulc files are fatal and their full paths appear in the
615
+ error. Missing cortex labels emit one warning and disable medial wall masking
616
+ for the affected hemisphere, so install or generate those labels before making
617
+ final figures.
618
+
619
+ ## Display interpolation and source-space resolution
620
+
621
+ `smoothing_steps=None` asks MNE to propagate sparse source values over surface
622
+ adjacency until the display mesh is covered. `smoothing_steps="nearest"`
623
+ reproduces the piecewise-constant legacy view. A non-negative integer requests
624
+ an exact number of propagation iterations; if it is too small for a dense mesh,
625
+ MNE warns rather than pretending the surface was completely filled.
626
+
627
+ Interpolation is applied to a one-time display copy. Numeric/percentile
628
+ thresholds, robust limits, and percent clim controls are computed from combined
629
+ left/right sparse values before expansion, so repeated dense vertices cannot
630
+ bias a percentile. The input STC is unchanged.
631
+
632
+ Very coarse inverse source spaces can remain visibly patchy even with automatic
633
+ interpolation. For scientifically smoother spatial detail, improve the
634
+ source-space resolution in the forward/inverse model or use an explicitly
635
+ reported analysis-stage smoothing method. Do not hide a low-resolution inverse
636
+ solution with undocumented plotting-only Gaussian blur.
637
+
638
+ ## Display threshold, color limits, and inference
639
+
640
+ A threshold is an optional hard display mask: values below it become fully
641
+ transparent after surface expansion. Abrupt masking can make isolated patches
642
+ look prominent. MNE-style clim dictionaries instead create a gradual
643
+ transparent-to-visible transition around peak regions.
644
+
645
+ The bundled audvis reference uses:
646
+
647
+ ```python
648
+ clim={"kind": "percent", "lims": [98, 99, 99.9]}
649
+ ```
650
+
651
+ It starts the ramp at the 98th percentile, reveals more localized activation
652
+ than the stricter p99 example, and avoids a cortex-wide red cast. Signed data
653
+ use `pos_lims` with a diverging palette.
654
+
655
+ Neither threshold nor clim performs a statistical significance test,
656
+ multiple-comparison correction, or inference. Determine significance in the
657
+ analysis pipeline, then choose display controls that faithfully communicate
658
+ that result. When color must be quantitatively comparable, use a shared clim
659
+ for every condition.
660
+
661
+ ## Layout, PNG output, and metadata
662
+
663
+ The row layout is `LH lateral | LH medial | RH medial | RH lateral`. The grid
664
+ places LH/RH lateral views on the first row and LH/RH medial views on the
665
+ second. BrainSpace uses global camera rotations internally; the anatomical
666
+ labels describe the hemisphere-relative view shown to the reader.
667
+
668
+ Static-only output returns `plotter=None`. Interactive or explicitly retained
669
+ scenes return a BrainSpace plotter or Nilearn Matplotlib `Figure`, according to
670
+ `backend`. The caller should close retained renderer objects. Returned
671
+ `selected_time` is always an actual STC sample, and `sampled_time_window`
672
+ records the first and last samples included in a window.
673
+
674
+ ## Reproduce the real-data acceptance set
675
+
676
+ The workflow follows MNE's
677
+ [source-estimate visualization tutorial](https://mne.tools/stable/auto_tutorials/inverse/60_visualize_stc.html).
678
+ With an existing MNE sample dataset:
679
+
680
+ ```bash
681
+ python examples/real_data_validation.py \
682
+ --sample-data-dir /path/to/MNE-sample-data \
683
+ --output-dir validation-output
684
+ ```
685
+
686
+ Or use an explicit two-hemisphere STC base and subject anatomy:
687
+
688
+ ```bash
689
+ python examples/real_data_validation.py \
690
+ --stc-base /data/fsaverage_audvis_trunc-meg \
691
+ --subjects-dir /data/subjects \
692
+ --subject fsaverage \
693
+ --output-dir validation-output
694
+ ```
695
+
696
+ The script creates fixed 112 ms, RMS 80-160 ms, inflated, pial, and shared-clim
697
+ comparison images plus `validation.json`. Its 0.65x comparison control checks
698
+ display consistency only; it is not an independent experimental condition.
699
+ `--download` opts into MNE's roughly 1.45 GB sample download. The default never
700
+ starts that download.
701
+
702
+ ## Compare the built-in BrainSpace and Nilearn renderers
703
+
704
+ Install the optional backend in an editable source checkout:
705
+
706
+ ```bash
707
+ python -m pip install -e ".[nilearn]"
708
+ ```
709
+
710
+ The repository keeps `comparison` as a compatibility alias for the controlled
711
+ acceptance workflow:
712
+
713
+ ```bash
714
+ python -m pip install -e ".[comparison]"
715
+ ```
716
+
717
+ Nilearn's `plot_img_on_surf` accepts a 3D Niimg-like volume and projects voxels
718
+ onto a surface. An MNE surface `SourceEstimate` already contains values at
719
+ cortical vertices, so converting it to a volume and projecting it back would
720
+ add interpolation and would not be a controlled renderer comparison. The
721
+ PySourceviz Nilearn backend therefore keeps the shared time selection, display
722
+ interpolation, medial-wall mask, threshold, colormap, and color range, then
723
+ passes the same vertex arrays to Nilearn's `plot_surf_stat_map`.
724
+
725
+ The often-cited `plot_img_on_surf` gallery image is a different data case. Its
726
+ 3D motor statistical map has negative and positive voxels, uses a diverging
727
+ `RdBu_r` palette, and projects multiple volumetric samples near the cortical
728
+ depth/normal. The audvis dSPM reference here is non-negative at the selected
729
+ sample, so the sequential `Reds` palette is scientifically appropriate rather
730
+ than inventing a blue negative branch.
731
+
732
+ Run the controlled comparison with explicit settings:
733
+
734
+ ```bash
735
+ python examples/compare_nilearn_backend.py \
736
+ --stc-base /data/fsaverage_audvis_trunc-meg \
737
+ --subjects-dir /data/subjects \
738
+ --subject fsaverage \
739
+ --time 0.112 \
740
+ --surface inflated \
741
+ --smoothing-steps auto \
742
+ --clim-percent 98 99 99.9 \
743
+ --mode magnitude \
744
+ --cmap Reds \
745
+ --output-dir renderer-comparison
746
+ ```
747
+
748
+ The comparison defaults to the recommended `surface="inflated"` and automatic
749
+ interpolation (`smoothing_steps=None`; CLI spelling `--smoothing-steps auto`).
750
+ It creates
751
+ `brainspace.png`, `nilearn.png`, and `comparison.json`. Both images share the
752
+ 1600 x 420 logical canvas, four-view order, source sample, activation array,
753
+ sulcal anatomy, cmap, and `(vmin, vmax)`. To inspect native folding explicitly,
754
+ pass `--surface pial`; to inspect the gray-white boundary, pass
755
+ `--surface white`.
756
+
757
+ The manifest records source/surface vertex counts, density ratio, versions,
758
+ pixel sizes, `display_value_range`, and `display_sign_counts`. BrainSpace/VTK
759
+ and Nilearn/Matplotlib can still look different because VTK shades point-data
760
+ actors, while Nilearn uses mean vertex-to-face averaging on triangular faces.
761
+ This affects surface appearance, not the prepared source values. Use
762
+ `plot_img_on_surf` only when the source result genuinely is a 3D NIfTI volume
763
+ and the volume-to-surface projection is scientifically intended.
764
+
765
+ ## Building and publishing
766
+
767
+ For an editable development environment with tests and release tools:
768
+
769
+ ```bash
770
+ python -m pip install -e ".[dev]"
771
+ python -m pytest -q
772
+ ```
773
+
774
+ The release helper resolves the project root from its own location. From the
775
+ repository root, run:
776
+
777
+ ```bash
778
+ ./scripts/release.sh build
779
+ ./scripts/release.sh testpypi --clean
780
+ # After installing and testing the TestPyPI candidate:
781
+ ./scripts/release.sh pypi --reuse
782
+ ```
783
+
784
+ Every target builds with PEP 517 unless `--reuse` is selected, and runs
785
+ Twine's strict metadata check before any upload. `--clean` removes only old
786
+ `.tar.gz` and `.whl` files. `--reuse` rechecks and uploads the exact existing
787
+ pair, so the PyPI release can match the tested TestPyPI artifacts. Production
788
+ upload asks for `publish`; use `--yes` only for an intentional non-interactive
789
+ release. Override the interpreter or output directory with
790
+ `PYSOURCEVIZ_PYTHON` or `PYSOURCEVIZ_DIST_DIR`. Without `--clean` or
791
+ `--reuse`, the helper refuses to overwrite existing distributions. Run
792
+ `./scripts/release.sh --help` for the complete command reference.
793
+
794
+ For the equivalent manual workflow, update the version in `pyproject.toml`,
795
+ make sure `dist/` contains no artifacts from an older version, then build both
796
+ the source distribution and wheel with the modern PEP 517 frontend:
797
+
798
+ The PyPI long description cannot resolve repository-relative images, so this
799
+ README uses GitHub Raw URLs. Always
800
+ push the release commit containing the gallery assets to GitHub before either
801
+ upload so all four result images are already reachable.
802
+
803
+ ```bash
804
+ python -m build
805
+ python -m twine check --strict dist/*
806
+ ```
807
+
808
+ Upload to TestPyPI first and install the candidate without resolving runtime
809
+ dependencies from the test index:
810
+
811
+ ```bash
812
+ python -m twine upload --repository testpypi dist/*
813
+ python -m pip install --index-url https://test.pypi.org/simple/ --no-deps PySourceviz
814
+ ```
815
+
816
+ After testing the candidate, publish the same checked artifacts to PyPI:
817
+
818
+ ```bash
819
+ python -m twine upload dist/*
820
+ ```
821
+
822
+ Enter an API token through Twine/keyring or the upload prompt; never write a
823
+ token into this repository. See the official
824
+ [PyPA packaging tutorial](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
825
+ for the standards-based build and upload flow.
826
+
827
+ ## Common errors
828
+
829
+ | Message or symptom | Meaning and action |
830
+ | --- | --- |
831
+ | `template='native' requires stc.subject` | Set the STC subject when creating/loading it, or use a correctly matching explicit template. |
832
+ | `STC subject space ... does not match` | Morph the STC externally with `mne.compute_source_morph`; do not relabel it. |
833
+ | `Required FreeSurfer surface file(s) missing` | Point `subjects_dir` one level above the subject and install the requested surface plus `lh/rh.sulc`. |
834
+ | `Cortex label file(s) missing` warning | Medial wall masking is disabled for that hemisphere; restore `lh/rh.cortex.label` before final export. |
835
+ | `STC has multiple time samples` | Set `time`, `time="peak"`, or `time_window`. |
836
+ | Large piecewise-constant activation patches | Use `smoothing_steps=None`; if a coarse source-space resolution remains visible, improve the inverse model instead of adding hidden plotting blur. |
837
+ | `output must use the .png extension` | Export PNG directly; convert formats afterward if needed. |
838
+ | `backend="nilearn" requires the optional Nilearn renderer` | Install `PySourceviz[nilearn]`, then rerun the same call. |
839
+ | Native VTK window/backend failure | Configure a supported VTK backend for the machine or render in a desktop session. |
840
+
841
+ ## Known limits
842
+
843
+ PySourceviz v0.1 renders real cortical scalar STCs, vector-surface magnitude,
844
+ and the surface portion of mixed STCs through BrainSpace or optional Nilearn.
845
+ It does not render volume-only or complex STCs, compute inverse solutions,
846
+ perform cross-subject morphing, or conduct statistical inference. Its
847
+ same-subject adjacency propagation is display interpolation; it must not be
848
+ interpreted as Gaussian/statistical smoothing or an increase in source-space
849
+ resolution.