tfce 0.1.0__tar.gz
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.
- tfce-0.1.0/LICENSE +30 -0
- tfce-0.1.0/MANIFEST.in +17 -0
- tfce-0.1.0/PKG-INFO +210 -0
- tfce-0.1.0/README.md +177 -0
- tfce-0.1.0/docs/README.md +35 -0
- tfce-0.1.0/docs/api.md +168 -0
- tfce-0.1.0/docs/benchmarks.md +152 -0
- tfce-0.1.0/docs/installation.md +78 -0
- tfce-0.1.0/docs/nilearn.md +107 -0
- tfce-0.1.0/docs/permutation.md +196 -0
- tfce-0.1.0/docs/theory.md +182 -0
- tfce-0.1.0/docs/usage.md +171 -0
- tfce-0.1.0/docs/validation.md +98 -0
- tfce-0.1.0/pyproject.toml +107 -0
- tfce-0.1.0/setup.cfg +4 -0
- tfce-0.1.0/setup.py +84 -0
- tfce-0.1.0/src/tfce/__init__.py +33 -0
- tfce-0.1.0/src/tfce/_c/tfce_batch.h +112 -0
- tfce-0.1.0/src/tfce/_c/tfce_capi.c +87 -0
- tfce-0.1.0/src/tfce/_c/tfce_capi.h +102 -0
- tfce-0.1.0/src/tfce/_c/tfce_maxtree.h +509 -0
- tfce-0.1.0/src/tfce/_c/tfce_threads.h +84 -0
- tfce-0.1.0/src/tfce/_maxtree.pyx +162 -0
- tfce-0.1.0/src/tfce/core.py +182 -0
- tfce-0.1.0/src/tfce/glm.py +171 -0
- tfce-0.1.0/src/tfce/nilearn_compat.py +88 -0
- tfce-0.1.0/src/tfce/tails.py +310 -0
- tfce-0.1.0/src/tfce.egg-info/PKG-INFO +210 -0
- tfce-0.1.0/src/tfce.egg-info/SOURCES.txt +38 -0
- tfce-0.1.0/src/tfce.egg-info/dependency_links.txt +1 -0
- tfce-0.1.0/src/tfce.egg-info/requires.txt +14 -0
- tfce-0.1.0/src/tfce.egg-info/top_level.txt +1 -0
- tfce-0.1.0/tests/data/matlab_reference.npz +0 -0
- tfce-0.1.0/tests/test_core.py +130 -0
- tfce-0.1.0/tests/test_docs.py +171 -0
- tfce-0.1.0/tests/test_exactness.py +122 -0
- tfce-0.1.0/tests/test_glm.py +131 -0
- tfce-0.1.0/tests/test_matlab_parity.py +67 -0
- tfce-0.1.0/tests/test_nilearn_compat.py +86 -0
- tfce-0.1.0/tests/test_tails.py +159 -0
tfce-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026, Christian Gaser
|
|
4
|
+
Structural Brain Mapping Group, Jena University Hospital
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
11
|
+
list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
18
|
+
contributors may be used to endorse or promote products derived from
|
|
19
|
+
this software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
22
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
23
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
25
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
26
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
27
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
28
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
29
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
tfce-0.1.0/MANIFEST.in
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# The C core is vendored into src/tfce/_c/ by setup.py. It MUST go into the sdist:
|
|
2
|
+
# without it there is nothing to compile, and `pip install tfce` from source fails.
|
|
3
|
+
include src/tfce/_c/*.c
|
|
4
|
+
include src/tfce/_c/*.h
|
|
5
|
+
include src/tfce/*.pyx
|
|
6
|
+
|
|
7
|
+
include README.md
|
|
8
|
+
include LICENSE
|
|
9
|
+
|
|
10
|
+
# the docs the PyPI page links to, so an sdist is self-documenting
|
|
11
|
+
recursive-include docs *.md
|
|
12
|
+
|
|
13
|
+
recursive-include tests *.py
|
|
14
|
+
recursive-include tests/data *.npz
|
|
15
|
+
|
|
16
|
+
exclude src/tfce/_maxtree.c
|
|
17
|
+
global-exclude *.so *.pyd __pycache__/*
|
tfce-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tfce
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Exact threshold-free cluster enhancement (TFCE) with no step size, and the permutation inference around it
|
|
5
|
+
Author-email: Christian Gaser <christian.gaser@uni-jena.de>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/ChristianGaser/tfce
|
|
8
|
+
Project-URL: Source, https://github.com/ChristianGaser/tfce
|
|
9
|
+
Project-URL: Issues, https://github.com/ChristianGaser/tfce/issues
|
|
10
|
+
Keywords: neuroimaging,TFCE,permutation,statistics,fMRI,VBM
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: C
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.22
|
|
22
|
+
Requires-Dist: scipy>=1.8
|
|
23
|
+
Provides-Extra: io
|
|
24
|
+
Requires-Dist: nibabel>=4.0; extra == "io"
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: nibabel>=4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: nilearn>=0.10; extra == "dev"
|
|
31
|
+
Requires-Dist: cython>=3.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# tfce
|
|
35
|
+
|
|
36
|
+
**Exact threshold-free cluster enhancement, and the permutation inference around it.**
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/tfce/)
|
|
39
|
+
[](https://pypi.org/project/tfce/)
|
|
40
|
+
[](https://github.com/ChristianGaser/tfce/blob/master/python/LICENSE)
|
|
41
|
+
|
|
42
|
+
TFCE combines focal effects of large height with broad effects of large extent, and needs **no
|
|
43
|
+
cluster-forming threshold** - the arbitrary choice that cluster-based inference forces on you, and
|
|
44
|
+
that the result can depend on heavily.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install tfce
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Wheels for Linux, macOS and Windows. No compiler needed.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Exact, not stepped
|
|
55
|
+
|
|
56
|
+
The TFCE of an element is an integral:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
TFCE(v) = ∫ e_v(h)^E · h^H dh
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
over the extent `e_v(h)` of the cluster containing `v` at height `h`. Implementations normally
|
|
63
|
+
approximate it by stepping `h` over a grid and summing. That costs a step size `dh`, a discretisation
|
|
64
|
+
error that depends on it, and an accuracy parameter you have to guess.
|
|
65
|
+
|
|
66
|
+
This one doesn't. It builds the **max-tree** (the component tree) with union-find, and because
|
|
67
|
+
`e_v(h)` is piecewise constant, integrates each piece in closed form. The answer is the integral, not
|
|
68
|
+
a sample of it. **There is nothing to tune.**
|
|
69
|
+
|
|
70
|
+
The difference is not academic. Against a stepped implementation on a 60×72×60 volume:
|
|
71
|
+
|
|
72
|
+
| steps | error vs exact |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| 50 | 3.4% |
|
|
75
|
+
| 100 | **1.7%** |
|
|
76
|
+
| 200 | 0.8% |
|
|
77
|
+
| 400 | 0.4% |
|
|
78
|
+
|
|
79
|
+
The error halves every time the steps double - first order, exactly as a step-size approximation must
|
|
80
|
+
behave. The exact transform has no such term.
|
|
81
|
+
|
|
82
|
+
## Quick start
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import numpy as np
|
|
86
|
+
import tfce
|
|
87
|
+
|
|
88
|
+
# a volume: (nx, ny, nz)
|
|
89
|
+
t = tfce.tfce(stat_map, E=0.5, H=2.0)
|
|
90
|
+
|
|
91
|
+
# a surface: faces are 1-based, as GIFTI stores them
|
|
92
|
+
adj = tfce.adjacency_from_faces(faces, n_vertices)
|
|
93
|
+
t = tfce.tfce(surf_map, adjacency=adj, E=1.0, H=2.0)
|
|
94
|
+
|
|
95
|
+
# a block of permutations, one per thread
|
|
96
|
+
# (nx, ny, nz, n_perm) -> (nx, ny, nz, n_perm)
|
|
97
|
+
t = tfce.tfce(perms, E=0.5, H=2.0, n_jobs=-1)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Volumes take `connectivity=6 | 18 | 26` (26 is the default, and what fslmaths and the MATLAB toolbox
|
|
101
|
+
use). Surfaces take the mesh, so the neighbourhood is whatever the mesh says it is.
|
|
102
|
+
|
|
103
|
+
Everything is arrays in, arrays out. No image objects, no file I/O, no design parsing - those belong
|
|
104
|
+
in a layer above, so the core can be dropped anywhere.
|
|
105
|
+
|
|
106
|
+
## Speed
|
|
107
|
+
|
|
108
|
+
Permutations are independent, so they are TFCE'd a block at a time, one per thread, with the GIL
|
|
109
|
+
released. On the same 60×72×60 volume:
|
|
110
|
+
|
|
111
|
+
| | one map | 16 permutations |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| stepped (100 steps) | 0.38 s | 9.1 s - 569 ms/perm |
|
|
114
|
+
| **tfce** | **0.03 s** (14×) | **0.13 s** - 8 ms/perm (**72×**) |
|
|
115
|
+
|
|
116
|
+
## Using it with nilearn
|
|
117
|
+
|
|
118
|
+
nilearn has `permuted_ols(..., tfce=True)`, and its TFCE is a stepped approximation. `tfce` ships a
|
|
119
|
+
drop-in with the same signature:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from nilearn.mass_univariate import _utils
|
|
123
|
+
import tfce.nilearn_compat as tc
|
|
124
|
+
|
|
125
|
+
_utils.calculate_tfce = tc.calculate_tfce # now exact, and much faster
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
It reads the neighbourhood out of the `bin_struct` it is handed, so it reproduces whichever
|
|
129
|
+
connectivity the caller meant.
|
|
130
|
+
|
|
131
|
+
## Fewer permutations
|
|
132
|
+
|
|
133
|
+
Counting exceedances cannot report a p-value below `1/n_perm`. That floor - not the statistic - is
|
|
134
|
+
what forces a permutation test to run many thousands of permutations, and it caps FDR too, since FDR
|
|
135
|
+
is computed from the uncorrected p-values. `tfce.tails` removes it:
|
|
136
|
+
|
|
137
|
+
- **Gamma** fit to the null of the *maximum*, for FWE-corrected p-values.
|
|
138
|
+
- **Generalised Pareto** fit to each element's own tail, for the uncorrected ones - with the *shape*
|
|
139
|
+
pooled across elements, since they all carry the same statistic under the same design and so differ
|
|
140
|
+
in scale, not in shape.
|
|
141
|
+
|
|
142
|
+
From 1000 permutations this recovers `p ≈ 1e-4` with the median right, where plain counting returns
|
|
143
|
+
zero for 91% of elements and tells you nothing at all.
|
|
144
|
+
|
|
145
|
+
## What's in the box
|
|
146
|
+
|
|
147
|
+
| | |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `tfce.core` | the exact transform - arrays in, arrays out |
|
|
150
|
+
| `tfce.tails` | Gamma and Generalised Pareto tail approximations |
|
|
151
|
+
| `tfce.glm` | the permuted GLM, which never forms the permuted data |
|
|
152
|
+
| `tfce.nilearn_compat` | drop-in for nilearn's `calculate_tfce` |
|
|
153
|
+
|
|
154
|
+
Dependencies are deliberately thin: **numpy and scipy**, nothing else. `nibabel` is an optional extra,
|
|
155
|
+
wanted only by I/O.
|
|
156
|
+
|
|
157
|
+
## Trust
|
|
158
|
+
|
|
159
|
+
This is not a reimplementation. It is the same C core that the
|
|
160
|
+
[MATLAB TFCE toolbox](https://github.com/ChristianGaser/tfce) has been running for years, with a
|
|
161
|
+
Cython binding instead of a MEX one - so the two give **bit-identical** results, and the test suite
|
|
162
|
+
holds them to it. It ships a validation suite of its own: the exactness of the max-tree is established
|
|
163
|
+
against an *independent* stepped implementation, which must converge onto it at first order.
|
|
164
|
+
|
|
165
|
+
## Citing
|
|
166
|
+
|
|
167
|
+
If you use this, please cite the method:
|
|
168
|
+
|
|
169
|
+
> Smith SM, Nichols TE (2009). *Threshold-free cluster enhancement: addressing problems of smoothing,
|
|
170
|
+
> threshold dependence and localisation in cluster inference.* NeuroImage 44:83–98.
|
|
171
|
+
> [doi:10.1016/j.neuroimage.2008.03.061](https://doi.org/10.1016/j.neuroimage.2008.03.061)
|
|
172
|
+
|
|
173
|
+
The tail approximations are from:
|
|
174
|
+
|
|
175
|
+
> Winkler AM, Ridgway GR, Douaud G, Nichols TE, Smith SM (2016). *Faster permutation inference in
|
|
176
|
+
> brain imaging.* NeuroImage 141:502–516.
|
|
177
|
+
> [doi:10.1016/j.neuroimage.2016.05.068](https://doi.org/10.1016/j.neuroimage.2016.05.068)
|
|
178
|
+
|
|
179
|
+
## Status
|
|
180
|
+
|
|
181
|
+
Early, but the parts that are here are tested and are the parts that matter. The transform, the tail
|
|
182
|
+
approximations and the permuted GLM are done. Not yet done: the **design layer** - turning a
|
|
183
|
+
[BIDS Stats Model](https://bids-standard.github.io/stats-models/) into a design matrix, contrasts and,
|
|
184
|
+
crucially, an *exchangeability* structure. That last one is the real work, because BIDS-SM specifies
|
|
185
|
+
`X`, `Formula`, `Contrasts` and `GroupBy` but has **no** concept of exchangeability blocks, so what may
|
|
186
|
+
be permuted with what has to be defined on top of it rather than read out of it.
|
|
187
|
+
|
|
188
|
+
## Documentation
|
|
189
|
+
|
|
190
|
+
| | |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| [Installation](https://github.com/ChristianGaser/tfce/blob/master/python/docs/installation.md) | Wheels, building from source, extras |
|
|
193
|
+
| [Usage](https://github.com/ChristianGaser/tfce/blob/master/python/docs/usage.md) | Volumes, surfaces, batches, connectivity, `E` and `H` |
|
|
194
|
+
| [A permutation test, end to end](https://github.com/ChristianGaser/tfce/blob/master/python/docs/permutation.md) | The whole thing, as a worked example you can run |
|
|
195
|
+
| [Theory](https://github.com/ChristianGaser/tfce/blob/master/python/docs/theory.md) | Why the integral is exact, and why you need fewer permutations |
|
|
196
|
+
| [Using it with nilearn](https://github.com/ChristianGaser/tfce/blob/master/python/docs/nilearn.md) | The drop-in - and a bug in nilearn's stepped TFCE |
|
|
197
|
+
| [API reference](https://github.com/ChristianGaser/tfce/blob/master/python/docs/api.md) | Every public function |
|
|
198
|
+
| [Benchmarks](https://github.com/ChristianGaser/tfce/blob/master/python/docs/benchmarks.md) | Accuracy and speed, with scripts to reproduce |
|
|
199
|
+
| [Validation](https://github.com/ChristianGaser/tfce/blob/master/python/docs/validation.md) | What the test suite establishes, and how |
|
|
200
|
+
|
|
201
|
+
## Links
|
|
202
|
+
|
|
203
|
+
- **Source, issues, MATLAB toolbox:** <https://github.com/ChristianGaser/tfce>
|
|
204
|
+
- **Licence:** BSD-3-Clause. Permissive on purpose: `nilearn` is BSD-3 and
|
|
205
|
+
`nipreps` is Apache-2.0, and neither can take on a GPL dependency. Nothing here
|
|
206
|
+
is derived from SPM, so nothing here has to be GPL. (The MATLAB/SPM toolbox in
|
|
207
|
+
the same repository *is* GPL, because it genuinely is derived from SPM - see
|
|
208
|
+
[LICENSE.md](https://github.com/ChristianGaser/tfce/blob/master/LICENSE.md).)
|
|
209
|
+
|
|
210
|
+
Developed by Christian Gaser, Structural Brain Mapping Group, Jena University Hospital.
|
tfce-0.1.0/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# tfce
|
|
2
|
+
|
|
3
|
+
**Exact threshold-free cluster enhancement, and the permutation inference around it.**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/tfce/)
|
|
6
|
+
[](https://pypi.org/project/tfce/)
|
|
7
|
+
[](https://github.com/ChristianGaser/tfce/blob/master/python/LICENSE)
|
|
8
|
+
|
|
9
|
+
TFCE combines focal effects of large height with broad effects of large extent, and needs **no
|
|
10
|
+
cluster-forming threshold** - the arbitrary choice that cluster-based inference forces on you, and
|
|
11
|
+
that the result can depend on heavily.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install tfce
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Wheels for Linux, macOS and Windows. No compiler needed.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Exact, not stepped
|
|
22
|
+
|
|
23
|
+
The TFCE of an element is an integral:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
TFCE(v) = ∫ e_v(h)^E · h^H dh
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
over the extent `e_v(h)` of the cluster containing `v` at height `h`. Implementations normally
|
|
30
|
+
approximate it by stepping `h` over a grid and summing. That costs a step size `dh`, a discretisation
|
|
31
|
+
error that depends on it, and an accuracy parameter you have to guess.
|
|
32
|
+
|
|
33
|
+
This one doesn't. It builds the **max-tree** (the component tree) with union-find, and because
|
|
34
|
+
`e_v(h)` is piecewise constant, integrates each piece in closed form. The answer is the integral, not
|
|
35
|
+
a sample of it. **There is nothing to tune.**
|
|
36
|
+
|
|
37
|
+
The difference is not academic. Against a stepped implementation on a 60×72×60 volume:
|
|
38
|
+
|
|
39
|
+
| steps | error vs exact |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| 50 | 3.4% |
|
|
42
|
+
| 100 | **1.7%** |
|
|
43
|
+
| 200 | 0.8% |
|
|
44
|
+
| 400 | 0.4% |
|
|
45
|
+
|
|
46
|
+
The error halves every time the steps double - first order, exactly as a step-size approximation must
|
|
47
|
+
behave. The exact transform has no such term.
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import numpy as np
|
|
53
|
+
import tfce
|
|
54
|
+
|
|
55
|
+
# a volume: (nx, ny, nz)
|
|
56
|
+
t = tfce.tfce(stat_map, E=0.5, H=2.0)
|
|
57
|
+
|
|
58
|
+
# a surface: faces are 1-based, as GIFTI stores them
|
|
59
|
+
adj = tfce.adjacency_from_faces(faces, n_vertices)
|
|
60
|
+
t = tfce.tfce(surf_map, adjacency=adj, E=1.0, H=2.0)
|
|
61
|
+
|
|
62
|
+
# a block of permutations, one per thread
|
|
63
|
+
# (nx, ny, nz, n_perm) -> (nx, ny, nz, n_perm)
|
|
64
|
+
t = tfce.tfce(perms, E=0.5, H=2.0, n_jobs=-1)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Volumes take `connectivity=6 | 18 | 26` (26 is the default, and what fslmaths and the MATLAB toolbox
|
|
68
|
+
use). Surfaces take the mesh, so the neighbourhood is whatever the mesh says it is.
|
|
69
|
+
|
|
70
|
+
Everything is arrays in, arrays out. No image objects, no file I/O, no design parsing - those belong
|
|
71
|
+
in a layer above, so the core can be dropped anywhere.
|
|
72
|
+
|
|
73
|
+
## Speed
|
|
74
|
+
|
|
75
|
+
Permutations are independent, so they are TFCE'd a block at a time, one per thread, with the GIL
|
|
76
|
+
released. On the same 60×72×60 volume:
|
|
77
|
+
|
|
78
|
+
| | one map | 16 permutations |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| stepped (100 steps) | 0.38 s | 9.1 s - 569 ms/perm |
|
|
81
|
+
| **tfce** | **0.03 s** (14×) | **0.13 s** - 8 ms/perm (**72×**) |
|
|
82
|
+
|
|
83
|
+
## Using it with nilearn
|
|
84
|
+
|
|
85
|
+
nilearn has `permuted_ols(..., tfce=True)`, and its TFCE is a stepped approximation. `tfce` ships a
|
|
86
|
+
drop-in with the same signature:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from nilearn.mass_univariate import _utils
|
|
90
|
+
import tfce.nilearn_compat as tc
|
|
91
|
+
|
|
92
|
+
_utils.calculate_tfce = tc.calculate_tfce # now exact, and much faster
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
It reads the neighbourhood out of the `bin_struct` it is handed, so it reproduces whichever
|
|
96
|
+
connectivity the caller meant.
|
|
97
|
+
|
|
98
|
+
## Fewer permutations
|
|
99
|
+
|
|
100
|
+
Counting exceedances cannot report a p-value below `1/n_perm`. That floor - not the statistic - is
|
|
101
|
+
what forces a permutation test to run many thousands of permutations, and it caps FDR too, since FDR
|
|
102
|
+
is computed from the uncorrected p-values. `tfce.tails` removes it:
|
|
103
|
+
|
|
104
|
+
- **Gamma** fit to the null of the *maximum*, for FWE-corrected p-values.
|
|
105
|
+
- **Generalised Pareto** fit to each element's own tail, for the uncorrected ones - with the *shape*
|
|
106
|
+
pooled across elements, since they all carry the same statistic under the same design and so differ
|
|
107
|
+
in scale, not in shape.
|
|
108
|
+
|
|
109
|
+
From 1000 permutations this recovers `p ≈ 1e-4` with the median right, where plain counting returns
|
|
110
|
+
zero for 91% of elements and tells you nothing at all.
|
|
111
|
+
|
|
112
|
+
## What's in the box
|
|
113
|
+
|
|
114
|
+
| | |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `tfce.core` | the exact transform - arrays in, arrays out |
|
|
117
|
+
| `tfce.tails` | Gamma and Generalised Pareto tail approximations |
|
|
118
|
+
| `tfce.glm` | the permuted GLM, which never forms the permuted data |
|
|
119
|
+
| `tfce.nilearn_compat` | drop-in for nilearn's `calculate_tfce` |
|
|
120
|
+
|
|
121
|
+
Dependencies are deliberately thin: **numpy and scipy**, nothing else. `nibabel` is an optional extra,
|
|
122
|
+
wanted only by I/O.
|
|
123
|
+
|
|
124
|
+
## Trust
|
|
125
|
+
|
|
126
|
+
This is not a reimplementation. It is the same C core that the
|
|
127
|
+
[MATLAB TFCE toolbox](https://github.com/ChristianGaser/tfce) has been running for years, with a
|
|
128
|
+
Cython binding instead of a MEX one - so the two give **bit-identical** results, and the test suite
|
|
129
|
+
holds them to it. It ships a validation suite of its own: the exactness of the max-tree is established
|
|
130
|
+
against an *independent* stepped implementation, which must converge onto it at first order.
|
|
131
|
+
|
|
132
|
+
## Citing
|
|
133
|
+
|
|
134
|
+
If you use this, please cite the method:
|
|
135
|
+
|
|
136
|
+
> Smith SM, Nichols TE (2009). *Threshold-free cluster enhancement: addressing problems of smoothing,
|
|
137
|
+
> threshold dependence and localisation in cluster inference.* NeuroImage 44:83–98.
|
|
138
|
+
> [doi:10.1016/j.neuroimage.2008.03.061](https://doi.org/10.1016/j.neuroimage.2008.03.061)
|
|
139
|
+
|
|
140
|
+
The tail approximations are from:
|
|
141
|
+
|
|
142
|
+
> Winkler AM, Ridgway GR, Douaud G, Nichols TE, Smith SM (2016). *Faster permutation inference in
|
|
143
|
+
> brain imaging.* NeuroImage 141:502–516.
|
|
144
|
+
> [doi:10.1016/j.neuroimage.2016.05.068](https://doi.org/10.1016/j.neuroimage.2016.05.068)
|
|
145
|
+
|
|
146
|
+
## Status
|
|
147
|
+
|
|
148
|
+
Early, but the parts that are here are tested and are the parts that matter. The transform, the tail
|
|
149
|
+
approximations and the permuted GLM are done. Not yet done: the **design layer** - turning a
|
|
150
|
+
[BIDS Stats Model](https://bids-standard.github.io/stats-models/) into a design matrix, contrasts and,
|
|
151
|
+
crucially, an *exchangeability* structure. That last one is the real work, because BIDS-SM specifies
|
|
152
|
+
`X`, `Formula`, `Contrasts` and `GroupBy` but has **no** concept of exchangeability blocks, so what may
|
|
153
|
+
be permuted with what has to be defined on top of it rather than read out of it.
|
|
154
|
+
|
|
155
|
+
## Documentation
|
|
156
|
+
|
|
157
|
+
| | |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| [Installation](https://github.com/ChristianGaser/tfce/blob/master/python/docs/installation.md) | Wheels, building from source, extras |
|
|
160
|
+
| [Usage](https://github.com/ChristianGaser/tfce/blob/master/python/docs/usage.md) | Volumes, surfaces, batches, connectivity, `E` and `H` |
|
|
161
|
+
| [A permutation test, end to end](https://github.com/ChristianGaser/tfce/blob/master/python/docs/permutation.md) | The whole thing, as a worked example you can run |
|
|
162
|
+
| [Theory](https://github.com/ChristianGaser/tfce/blob/master/python/docs/theory.md) | Why the integral is exact, and why you need fewer permutations |
|
|
163
|
+
| [Using it with nilearn](https://github.com/ChristianGaser/tfce/blob/master/python/docs/nilearn.md) | The drop-in - and a bug in nilearn's stepped TFCE |
|
|
164
|
+
| [API reference](https://github.com/ChristianGaser/tfce/blob/master/python/docs/api.md) | Every public function |
|
|
165
|
+
| [Benchmarks](https://github.com/ChristianGaser/tfce/blob/master/python/docs/benchmarks.md) | Accuracy and speed, with scripts to reproduce |
|
|
166
|
+
| [Validation](https://github.com/ChristianGaser/tfce/blob/master/python/docs/validation.md) | What the test suite establishes, and how |
|
|
167
|
+
|
|
168
|
+
## Links
|
|
169
|
+
|
|
170
|
+
- **Source, issues, MATLAB toolbox:** <https://github.com/ChristianGaser/tfce>
|
|
171
|
+
- **Licence:** BSD-3-Clause. Permissive on purpose: `nilearn` is BSD-3 and
|
|
172
|
+
`nipreps` is Apache-2.0, and neither can take on a GPL dependency. Nothing here
|
|
173
|
+
is derived from SPM, so nothing here has to be GPL. (The MATLAB/SPM toolbox in
|
|
174
|
+
the same repository *is* GPL, because it genuinely is derived from SPM - see
|
|
175
|
+
[LICENSE.md](https://github.com/ChristianGaser/tfce/blob/master/LICENSE.md).)
|
|
176
|
+
|
|
177
|
+
Developed by Christian Gaser, Structural Brain Mapping Group, Jena University Hospital.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# tfce - documentation
|
|
2
|
+
|
|
3
|
+
Exact threshold-free cluster enhancement, and the permutation inference around it.
|
|
4
|
+
|
|
5
|
+
The [package README](../README.md) is the two-minute version. These pages are the rest.
|
|
6
|
+
|
|
7
|
+
## Start here
|
|
8
|
+
|
|
9
|
+
| | |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| **[Installation](installation.md)** | `pip install tfce`, wheels, building from source, optional extras. |
|
|
12
|
+
| **[Usage](usage.md)** | Volumes, surfaces, batches of permutations, connectivity, `E` and `H`. |
|
|
13
|
+
| **[A permutation test, end to end](permutation.md)** | The whole thing: GLM → TFCE → FWE and uncorrected p-values. A worked example you can run. |
|
|
14
|
+
|
|
15
|
+
## Going deeper
|
|
16
|
+
|
|
17
|
+
| | |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| **[Theory](theory.md)** | Why the integral is exact, what a max-tree is, and why the tail approximations mean you can run far fewer permutations. |
|
|
20
|
+
| **[Using it with nilearn](nilearn.md)** | The drop-in, the connectivity mismatch, and a bug in nilearn's stepped TFCE worth knowing about. |
|
|
21
|
+
| **[API reference](api.md)** | Every public function, with its arguments and what they do. |
|
|
22
|
+
| **[Benchmarks](benchmarks.md)** | Accuracy and speed, with the scripts to reproduce them. |
|
|
23
|
+
| **[Validation](validation.md)** | What the test suite establishes, and how it establishes it. |
|
|
24
|
+
|
|
25
|
+
## The one-paragraph summary
|
|
26
|
+
|
|
27
|
+
The TFCE of an element is an integral over the extent of the cluster containing it, taken across all
|
|
28
|
+
heights. Implementations normally approximate that integral by stepping the height over a grid and
|
|
29
|
+
summing, which costs a step size, a discretisation error that depends on it, and an accuracy
|
|
30
|
+
parameter the caller has to guess. This one builds the **max-tree** (the component tree) with
|
|
31
|
+
union-find and integrates each piece of the extent function in closed form, because that function is
|
|
32
|
+
piecewise constant. The result is the integral, not a sample of it, and there is nothing to tune.
|
|
33
|
+
|
|
34
|
+
It is the same C the [MATLAB TFCE toolbox](https://github.com/ChristianGaser/tfce) runs - not a
|
|
35
|
+
reimplementation - so the two are bit-identical, and the test suite holds them to it.
|
tfce-0.1.0/docs/api.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
Everything public, with what it takes and what it gives back.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import tfce
|
|
7
|
+
from tfce.glm import PermutedGLM, partition_design
|
|
8
|
+
from tfce.tails import gamma_pvalue, pareto_pvalue, gpd_fit_pwm, gpd_sf
|
|
9
|
+
from tfce.nilearn_compat import calculate_tfce, connectivity_from_bin_struct
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## `tfce`
|
|
15
|
+
|
|
16
|
+
### `tfce(data, *, adjacency=None, connectivity=26, E=0.5, H=2.0, two_sided=True, n_jobs=1)`
|
|
17
|
+
|
|
18
|
+
Threshold-free cluster enhancement of one map or of many.
|
|
19
|
+
|
|
20
|
+
| argument | | |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `data` | ndarray | `(nx, ny, nz)` or `(nx, ny, nz, B)` for volumes; `(n_vertices,)` or `(n_vertices, B)` for surfaces. Any dtype - used as `float32`. |
|
|
23
|
+
| `adjacency` | sparse or `(indptr, indices)` | The mesh, for surface data. Omit for volumes. |
|
|
24
|
+
| `connectivity` | `6`, `18`, `26` | Volume neighbourhood. Ignored for surfaces. |
|
|
25
|
+
| `E`, `H` | float | Extent and height weights. `0.5, 2` for volumes; `1, 2` for surfaces and TBSS. |
|
|
26
|
+
| `two_sided` | bool | Also enhance the negative part, giving it a negative TFCE value. |
|
|
27
|
+
| `n_jobs` | int | Threads across the `B` maps. `-1` = one per map. Releases the GIL. |
|
|
28
|
+
|
|
29
|
+
**Returns** `float32` array in the shape the data came in.
|
|
30
|
+
|
|
31
|
+
**Raises** `ValueError` on a bad shape or connectivity; `TfceError` if the C core refuses the call.
|
|
32
|
+
|
|
33
|
+
### `adjacency_from_faces(faces, n_vertices)`
|
|
34
|
+
|
|
35
|
+
CSR adjacency of a triangulated surface.
|
|
36
|
+
|
|
37
|
+
| argument | | |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `faces` | `(n_faces, 3)` int | **1-based** vertex indices, as GIFTI stores them. |
|
|
40
|
+
| `n_vertices` | int | |
|
|
41
|
+
|
|
42
|
+
**Returns** `(indptr, indices)`, both `int32` and 0-based - ready for `scipy.sparse.csr_matrix`, or to
|
|
43
|
+
pass straight back to `tfce()` as `adjacency`.
|
|
44
|
+
|
|
45
|
+
**Raises** `TfceError` if any index names a vertex that does not exist. This is a real check, not a
|
|
46
|
+
formality: an out-of-range index used to be written past the end of the adjacency arrays.
|
|
47
|
+
|
|
48
|
+
### `TfceError`
|
|
49
|
+
|
|
50
|
+
Raised when the C core refuses a call. Subclasses `RuntimeError`.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## `tfce.tails`
|
|
55
|
+
|
|
56
|
+
### `gamma_pvalue(stat, null_max, floor=None)`
|
|
57
|
+
|
|
58
|
+
FWE-corrected p-values, from a Gamma fitted to the distribution of the **maximum**.
|
|
59
|
+
|
|
60
|
+
| argument | | |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `stat` | ndarray | Observed statistic, one value per element. |
|
|
63
|
+
| `null_max` | `(n_perm,)` | The maximum of the statistic over the image, one per permutation. |
|
|
64
|
+
| `floor` | float | Smallest p-value to report. Defaults to `1/n_perm`. |
|
|
65
|
+
|
|
66
|
+
**Returns** p-values, same shape as `stat`.
|
|
67
|
+
|
|
68
|
+
The Gamma is fitted by matching the first three moments, which inverts in closed form. If the sample
|
|
69
|
+
skewness comes out non-positive there is nothing for a Gamma to hold on to, and the empirical count is
|
|
70
|
+
returned instead.
|
|
71
|
+
|
|
72
|
+
### `pareto_pvalue(stat, tail, cnt, n_perm, *, n_exc_min=25, n_shape=20000, ad_max=5.0)`
|
|
73
|
+
|
|
74
|
+
Uncorrected p-values, resolved **below** the `1/n_perm` floor of counting.
|
|
75
|
+
|
|
76
|
+
| argument | | |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `stat` | `(n_elements,)` | Observed statistic, **already an upper tail** - flip the sign of the negative elements first. |
|
|
79
|
+
| `tail` | `(K, n_elements)` | The largest `K` permuted values of each element. `K ≈ 100` is plenty. |
|
|
80
|
+
| `cnt` | `(n_elements,)` | How often the permutations reached or exceeded `stat`, counted over **all** `n_perm` - not just the ones in `tail`. |
|
|
81
|
+
| `n_perm` | int | How many permutations were drawn. |
|
|
82
|
+
| `n_exc_min` | int | Elements exceeded at least this often keep their count; the fit has nothing to add. |
|
|
83
|
+
| `n_shape` | int | How many elements the shape is pooled over. |
|
|
84
|
+
| `ad_max` | float | A tail no candidate fit describes this well is counted, not extrapolated. |
|
|
85
|
+
|
|
86
|
+
**Returns** p-values, `(n_elements,)`.
|
|
87
|
+
|
|
88
|
+
> **Why `cnt` and `n_perm` are arguments.** Storing the whole `n_perm × n_elements` null is impossible
|
|
89
|
+
> at any real size, so only the tail is kept - but a truncated tail cannot say *how often* an element
|
|
90
|
+
> was exceeded, and that count is what the p-value is made of. Accumulate it in your permutation loop:
|
|
91
|
+
> it costs one comparison per element. See [permutation.md](permutation.md).
|
|
92
|
+
|
|
93
|
+
### `gpd_fit_pwm(y)`
|
|
94
|
+
|
|
95
|
+
Generalised Pareto parameters by probability-weighted moments (Hosking & Wallis).
|
|
96
|
+
|
|
97
|
+
`y` is `(m, n)` - exceedances above a threshold, **ascending** down each column, one column per
|
|
98
|
+
element. Returns `(k, sigma)`, each `(n,)`, parameterised as `F(y) = 1 − (1 − k·y/σ)^(1/k)`.
|
|
99
|
+
|
|
100
|
+
### `gpd_sf(z, k, sigma)`
|
|
101
|
+
|
|
102
|
+
Survival function of that distribution. Handles the `k → 0` limit (exponential) and clamps to zero
|
|
103
|
+
beyond the upper endpoint of a `k > 0` fit.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## `tfce.glm`
|
|
108
|
+
|
|
109
|
+
### `PermutedGLM(Y, design, contrast)`
|
|
110
|
+
|
|
111
|
+
Fit `Y = X·B`, and re-fit it under permutations **without ever forming the permuted data**.
|
|
112
|
+
|
|
113
|
+
| argument | | |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `Y` | `(n_elements, n_samples)` | One **row** per element, one column per observation. Note the orientation: elements are the long axis and this keeps them contiguous. |
|
|
116
|
+
| `design` | `(n_samples, n_regressors)` | |
|
|
117
|
+
| `contrast` | `(n_regressors,)` or `(n_regressors, q)` | A vector gives a t-contrast, a matrix an F-contrast. |
|
|
118
|
+
|
|
119
|
+
**Attributes**: `df`, `rank`, `pinvX`, `XtX`, `ssy`, `is_t`.
|
|
120
|
+
|
|
121
|
+
**Raises** `ValueError` if the design leaves no residual degrees of freedom, or if `Y` and the design
|
|
122
|
+
disagree about the number of observations.
|
|
123
|
+
|
|
124
|
+
#### `.fit(perm=None)`
|
|
125
|
+
|
|
126
|
+
Statistic under one permutation. `perm` is read the way numpy reads a fancy index: the permuted data
|
|
127
|
+
is `Y[:, perm]`. `None` is the identity - the unpermuted fit.
|
|
128
|
+
|
|
129
|
+
**Returns** `(n_elements,)`.
|
|
130
|
+
|
|
131
|
+
#### `.fit_signs(signs)`
|
|
132
|
+
|
|
133
|
+
Statistic under one sign-flip, for a one-sample design. `signs` is `(n_samples,)` of ±1.
|
|
134
|
+
|
|
135
|
+
A one-sample design is not permuted by shuffling - there is nothing to shuffle it with. It is permuted
|
|
136
|
+
by flipping signs, which is exchangeable under the null of "mean zero".
|
|
137
|
+
|
|
138
|
+
### `partition_design(design, contrast)`
|
|
139
|
+
|
|
140
|
+
Guttman partition: which columns the contrast tests, and which are nuisance.
|
|
141
|
+
|
|
142
|
+
**Returns** `(idx_interest, idx_nuisance)`.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## `tfce.nilearn_compat`
|
|
147
|
+
|
|
148
|
+
### `calculate_tfce(arr4d, bin_struct, E=0.5, H=2, dh="auto", two_sided_test=True)`
|
|
149
|
+
|
|
150
|
+
Drop-in for `nilearn.mass_univariate._utils.calculate_tfce`, with the same signature and an exact
|
|
151
|
+
transform. See [nilearn.md](nilearn.md).
|
|
152
|
+
|
|
153
|
+
`dh` is **accepted and ignored** - there is no step size. Passing one is not an error; it simply has
|
|
154
|
+
nothing to change.
|
|
155
|
+
|
|
156
|
+
### `connectivity_from_bin_struct(bin_struct)`
|
|
157
|
+
|
|
158
|
+
Read `6`, `18` or `26` out of a `scipy.ndimage.generate_binary_structure` array, by counting the
|
|
159
|
+
neighbours it actually marks.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Module-level
|
|
164
|
+
|
|
165
|
+
| | |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `tfce.__version__` | the installed version |
|
|
168
|
+
| `tfce.core.VALID_CONNECTIVITY` | `(6, 18, 26)` |
|