indi-net-extract 0.1.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.
- indi_net_extract-0.1.0.dev0.dist-info/METADATA +158 -0
- indi_net_extract-0.1.0.dev0.dist-info/RECORD +18 -0
- indi_net_extract-0.1.0.dev0.dist-info/WHEEL +5 -0
- indi_net_extract-0.1.0.dev0.dist-info/licenses/LICENSE +30 -0
- indi_net_extract-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +30 -0
- indi_net_extract-0.1.0.dev0.dist-info/top_level.txt +1 -0
- indinetextract/__init__.py +13 -0
- indinetextract/_algorithms/__init__.py +1 -0
- indinetextract/_algorithms/dual_regression.py +148 -0
- indinetextract/_algorithms/gig_ica.py +412 -0
- indinetextract/_io.py +302 -0
- indinetextract/_types.py +114 -0
- indinetextract/_validation.py +124 -0
- indinetextract/_version.py +1 -0
- indinetextract/batch.py +218 -0
- indinetextract/evaluation.py +129 -0
- indinetextract/extraction.py +326 -0
- indinetextract/postprocessing.py +391 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: indi-net-extract
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: Template-guided individual network extraction and spatial overlap evaluation
|
|
5
|
+
License-Expression: BSD-3-Clause
|
|
6
|
+
Project-URL: Source, https://github.com/sonia1maslova/indi-net-extract
|
|
7
|
+
Project-URL: Documentation, https://github.com/sonia1maslova/indi-net-extract/blob/main/README.md
|
|
8
|
+
Project-URL: Issues, https://github.com/sonia1maslova/indi-net-extract/issues
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
16
|
+
Requires-Dist: numpy>=1.26
|
|
17
|
+
Requires-Dist: scipy>=1.11
|
|
18
|
+
Requires-Dist: nibabel>=5.2
|
|
19
|
+
Requires-Dist: threadpoolctl>=3.1
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# IndiNetExtract
|
|
23
|
+
|
|
24
|
+
Python分发名(安装名):`indi-net-extract`。Python导入名和源码包目录:`indinetextract`。
|
|
25
|
+
|
|
26
|
+
模板引导的个体网络提取工具包,开发初版 `0.1.0.dev0`。目前提供Python源码,尚未发布到PyPI。
|
|
27
|
+
|
|
28
|
+
输入被试ID列表、逐项对应的静息态影像路径,以及全体共用的模板和mask。输出每名被试的连续网络图、时间序列及运行记录。支持经典双回归β和独立NumPy/SciPy GIG-ICA。
|
|
29
|
+
|
|
30
|
+
**每名被试只输入一份已经准备好的4D NIfTI。** 数据清洗、扫描标准化、拼接和空间配准在包外完成。批处理列表表示不同被试,不表示要拼接的扫描。算法内部所需的中心化、白化和双回归时间序列归一化仍保留。
|
|
31
|
+
|
|
32
|
+
## 从源码安装
|
|
33
|
+
|
|
34
|
+
当前为开发初版,尚未发布到PyPI。可从本仓库安装:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/sonia1maslova/indi-net-extract.git
|
|
38
|
+
cd indi-net-extract
|
|
39
|
+
python -m pip install .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 快速使用
|
|
43
|
+
|
|
44
|
+
在已具备NumPy、SciPy、NiBabel、threadpoolctl的环境中,将本包 `src` 加入 `PYTHONPATH`,或安装本地构建产物。已测试的软件版本和验证边界见 `docs/validation.md`;以下示例不下载或修改数据。
|
|
45
|
+
|
|
46
|
+
安装已下载的wheel时,文件名中的分发名按打包规则写成 `indi_net_extract`:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python -m pip install /path/to/indi_net_extract-0.1.0.dev0-py3-none-any.whl
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
安装后仍使用 `from indinetextract import ...`。分发名中的连字符不用于Python导入语句。
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from indinetextract import extract_batch
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
batch = extract_batch(
|
|
59
|
+
subject_ids=["sub-001", "sub-002"],
|
|
60
|
+
fmri_paths=[
|
|
61
|
+
"/data/sub-001/rest_preprocessed.nii.gz",
|
|
62
|
+
"/data/sub-002/rest_preprocessed.nii.gz",
|
|
63
|
+
],
|
|
64
|
+
templates="/data/templates.nii.gz",
|
|
65
|
+
mask="/data/analysis_mask.nii.gz",
|
|
66
|
+
output_root="/results/gig_ica",
|
|
67
|
+
method="gig_ica",
|
|
68
|
+
n_jobs=2,
|
|
69
|
+
n_threads=2,
|
|
70
|
+
)
|
|
71
|
+
print(batch.summary)
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
并行采用独立进程,脚本需要上面的 `__main__` 保护。只处理一名时,也传入长度为1的两个列表。输入文件名不必含被试ID;包按列表位置配对,不推测文件属于谁。
|
|
78
|
+
|
|
79
|
+
| 参数 | 含义 |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `subject_ids` | 非空且唯一的字符串ID列表 |
|
|
82
|
+
| `fmri_paths` | 等长路径列表,每条路径是一名被试的一份4D影像 |
|
|
83
|
+
| `templates` | 同一份4D空间模板,第四维为K个成分 |
|
|
84
|
+
| `mask` | 同一份3D二值分析mask |
|
|
85
|
+
| `method` | `"dual_regression"` 或 `"gig_ica"` |
|
|
86
|
+
| `n_jobs=1` | 最多同时处理的被试数,即进程数 |
|
|
87
|
+
| `n_threads=2` | 每名被试数值计算的线程预算 |
|
|
88
|
+
| `label="extraction"` | 被试目录下的本次输出标签;可用不同标签区分方法、模板或测量日 |
|
|
89
|
+
| `on_error="continue"` | 记录失败后继续;`"raise"` 表示停止提交新被试,等待正在运行的被试结束后报错 |
|
|
90
|
+
| `work_dir=None` | 可选临时存储目录;每名被试使用独立临时子目录 |
|
|
91
|
+
|
|
92
|
+
例如 `n_jobs=3, n_threads=2` 表示最多3名被试同时拟合,每名的数值运算预算为2线程。它不保证总内存足够;GIG-ICA仍需白化和SVD工作内存。默认串行执行,不自动增加进程。
|
|
93
|
+
|
|
94
|
+
## 输出
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
output_root/
|
|
98
|
+
_batches/<batch_id>/
|
|
99
|
+
inputs.tsv # 保存原ID—影像配对
|
|
100
|
+
summary.tsv # 顺序与输入一致,包含状态及失败原因
|
|
101
|
+
metadata.json # 共用设置、环境及源码指纹
|
|
102
|
+
sub-001/extraction/
|
|
103
|
+
continuous_components.nii.gz # X×Y×Z×K,连续成分
|
|
104
|
+
timecourses.tsv # T×K,行是时间点,无标题行
|
|
105
|
+
components.tsv # 0起始图像索引与成分ID
|
|
106
|
+
mask.nii.gz
|
|
107
|
+
metadata.json
|
|
108
|
+
sub-002/extraction/
|
|
109
|
+
...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
连续图的第四维是**成分**,不是时间;第i张图对应第i个模板。默认成分ID为C1…CK。双回归输出空间β,GIG-ICA输出成分权重,两者不当作统计Z或概率。
|
|
113
|
+
|
|
114
|
+
一名完成后立即保存。`BatchResult`只返回轻量记录和文件路径,不把全体影像留在内存。失败被试不产生一个伪成功的结果目录。默认不覆盖已有结果;显式调用结果对象的 `save(overwrite=True)` 时,旧目录改名为备份并保留。
|
|
115
|
+
|
|
116
|
+
## 独立后处理与模板Dice
|
|
117
|
+
|
|
118
|
+
已经提取好的连续图可以重新载入,改变平滑或Top-N时不用重新拟合:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from indinetextract import NetworkResult, postprocess_maps, template_dice, overlap_fraction
|
|
122
|
+
|
|
123
|
+
fit = NetworkResult.load("/results/gig_ica/sub-001/extraction")
|
|
124
|
+
binary = postprocess_maps(
|
|
125
|
+
fit,
|
|
126
|
+
fwhm_mm=6,
|
|
127
|
+
count_reference="/data/fixed_binary_reference.nii.gz",
|
|
128
|
+
)
|
|
129
|
+
binary.save("/results/gig_ica", label="fwhm_6mm")
|
|
130
|
+
|
|
131
|
+
dice = template_dice(binary, reference="/data/fixed_binary_reference.nii.gz")
|
|
132
|
+
print(dice.per_component, dice.mean_dice)
|
|
133
|
+
dice.save("/results/gig_ica", label="fwhm_6mm/template_dice")
|
|
134
|
+
|
|
135
|
+
pleiotropy = overlap_fraction(binary) # 3D NIfTI对象,值为所选成分比例
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- `count_reference`必须是二值图,在mask内计数得到每成分N。也可以改用显式的 `top_n=[...]`,两者必须二选一。
|
|
139
|
+
- 用于提取的模板允许连续值;用于计数和Dice的参考图必须明确为0/1。切换提取模板不自动改变N。
|
|
140
|
+
- 平滑使用 `G(M*f)/G(M)`,仅沿空间维,保留原mask。每个宽度都从原始连续图开始。NIfTI空间单位unknown时按mm解释并记录;正宽度平滑拒绝剪切网格。
|
|
141
|
+
- Top-N按有限的有符号数值降序选严格N个,允许零值和负值;并列按原始Fortran体素索引。候选不足时报错,不自动缩小N。
|
|
142
|
+
- Dice矩阵行是个体成分,列是参考成分;对应分数是对角线。不自动换序、翻符号或选择最佳配对。
|
|
143
|
+
- 单侧为空时Dice为0,双侧为空为NaN;存在无效对应成分时完整K成分均值为NaN。JSON中缺失数值保存为null,TSV中为nan。
|
|
144
|
+
- `overlap_fraction`为K张二值图的逐体素平均;它不自动构成统计显著性检验。
|
|
145
|
+
|
|
146
|
+
## 输入约束与验证边界
|
|
147
|
+
|
|
148
|
+
所有影像必须事先对齐,空间形状一致,affine数值在记录的容差内一致。mask显式提供,保持不变。mask外不参与计算;mask内NaN/Inf或时间常量BOLD序列会报错,包不自动插补或删除体素。
|
|
149
|
+
|
|
150
|
+
独立Python GIG核心保留原算法源码许可与控制流程。此前合成数据及三名真实被试有MATLAB对照,真实数据不是所有数值严格相等。此包封装的验证范围和具体检查见 [docs/validation.md](docs/validation.md);不能只依据模板Dice较高就判定个体网络科学有效。
|
|
151
|
+
|
|
152
|
+
当前实际验证使用Python3.13项目环境;其他Python版本和Windows原生并行尚需分别测试。声明最低Python版本不等于所有组合均已测试。
|
|
153
|
+
|
|
154
|
+
自动单元测试不依赖MATLAB或FSL;本轮另外完成了两种模板的小型FSL对照。更新算法一致性结论时,应保留输入、源码、参数和数值误差。
|
|
155
|
+
|
|
156
|
+
## 来源与许可
|
|
157
|
+
|
|
158
|
+
GIG数值核心位于 `_algorithms/gig_ica.py`,来自已有NumPy/SciPy移植;原始来源为GIFT `icatb_gigicar.m`,作者Yuhui Du、Yong Fan。版权与许可保留在文件头及 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。其他初版代码采用BSD-3-Clause许可。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
indi_net_extract-0.1.0.dev0.dist-info/licenses/LICENSE,sha256=eKxG5ZOSYC1UrpeitDpfrh6nrpNflHKeCn4p1O7me-I,1669
|
|
2
|
+
indi_net_extract-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md,sha256=Fo5TezIcIOn2hEa0OwmBOSyrsCf5S9M9MwmGjrxxxgk,1719
|
|
3
|
+
indinetextract/__init__.py,sha256=QL3hfkGPGMCtfX3TU8_eLKEvSDxwElWMAL42OqnQg6g,569
|
|
4
|
+
indinetextract/_io.py,sha256=a9ekGsf7oJLOf-FYaCrQjRSB3sW4G9WNiNzBscJBNFc,14510
|
|
5
|
+
indinetextract/_types.py,sha256=Js8smkrVZJDRJk3C5ARJopT5aHwXiZjoucd1yHNRF0I,3406
|
|
6
|
+
indinetextract/_validation.py,sha256=_mQ9lV4mz_6klC6hHFLK6nYlkyXEjyT_knYYuOBUGcY,5445
|
|
7
|
+
indinetextract/_version.py,sha256=qbhMh4mg2JMoCFflKwIRvApU63RR8AxvYq1OT__8sgI,27
|
|
8
|
+
indinetextract/batch.py,sha256=N4am7m3K_f8mIsSbhsnjEIsTa7BrHN0w6D6fOIl_pNw,10719
|
|
9
|
+
indinetextract/evaluation.py,sha256=hQMhlPxbcfAnwY9XoRqJXq8kEhlbl_PzvAL9W2OLIzA,4801
|
|
10
|
+
indinetextract/extraction.py,sha256=Vq49g2-V4Y0p2lFpgtJ5t94bfuIGnsFH7epST_gJ62I,15377
|
|
11
|
+
indinetextract/postprocessing.py,sha256=fAu8BDJeANElHp2m60mWzZ2H-vtfotdB5k1l-bWy9lE,15537
|
|
12
|
+
indinetextract/_algorithms/__init__.py,sha256=wV0sILyqC0eMyMbtIMJ5F_17EN0vOELFrtNs8MH_TCo,73
|
|
13
|
+
indinetextract/_algorithms/dual_regression.py,sha256=qhoqnsysTF_qc-WFj1QUQYhjmz45GAFgpKAys-C35Sg,7076
|
|
14
|
+
indinetextract/_algorithms/gig_ica.py,sha256=dqsah-d7rhxbevQXEegxLIb6oH52DpYnqK515F38fSM,20954
|
|
15
|
+
indi_net_extract-0.1.0.dev0.dist-info/METADATA,sha256=MebRIXr0dkTJskR4Lzfl1E8IgEEuq64sDRwxy7-is7g,8240
|
|
16
|
+
indi_net_extract-0.1.0.dev0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
17
|
+
indi_net_extract-0.1.0.dev0.dist-info/top_level.txt,sha256=RP2TrCaf6MdiDkrin64Bx7L4bUnGBZpfaoU6UvYu3N8,15
|
|
18
|
+
indi_net_extract-0.1.0.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, indinetextract contributors
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer.
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
15
|
+
may be used to endorse or promote products derived from this software
|
|
16
|
+
without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
|
|
29
|
+
The GIG-ICA source additionally retains its original notice; see
|
|
30
|
+
THIRD_PARTY_NOTICES.md and src/indinetextract/_algorithms/gig_ica.py.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Third-party source notices
|
|
2
|
+
|
|
3
|
+
The GIG-ICA Python core retains the following original notice. Original source: GIFT icatb_gigicar.m; source SHA256: 6a0bbf665524d0122686ce6416d06350cb21d71f90460c282aa539f76803773c.
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2012 Yuhui DU and Yong FAN
|
|
6
|
+
All rights reserved.
|
|
7
|
+
|
|
8
|
+
Redistribution and use in source or any other forms, with or without
|
|
9
|
+
modification, are permitted provided that the following conditions are met:
|
|
10
|
+
|
|
11
|
+
* Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer.
|
|
13
|
+
* Redistributions in any other 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
|
+
* Neither the names of the copyright holders nor the names of future
|
|
17
|
+
contributors may be used to endorse or promote products derived from this
|
|
18
|
+
software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
23
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
24
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
25
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
26
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
27
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
28
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
29
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
30
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
indinetextract
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Template-guided extraction from already prepared resting-state fMRI."""
|
|
2
|
+
from ._version import __version__
|
|
3
|
+
from ._types import (BatchExecutionError, BatchResult, BinaryMapsResult,
|
|
4
|
+
DiceResult, NetworkResult)
|
|
5
|
+
from .batch import extract_batch
|
|
6
|
+
from .postprocessing import postprocess_maps
|
|
7
|
+
from .evaluation import overlap_fraction, template_dice
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"extract_batch", "postprocess_maps", "template_dice", "overlap_fraction",
|
|
11
|
+
"NetworkResult", "BinaryMapsResult", "DiceResult", "BatchResult",
|
|
12
|
+
"BatchExecutionError", "__version__",
|
|
13
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Private numerical implementations with explicit source provenance."""
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Minimal classic dual regression, following the project's stock-FSL check.
|
|
2
|
+
|
|
3
|
+
The stage-one template normalization, accumulated cross-products, SVD-based
|
|
4
|
+
inverse Gram, stage-two intercept and normal-equation solver are adapted from
|
|
5
|
+
``dr_improvements_20261031/dr_core.py``. The selected route is exactly the
|
|
6
|
+
``official_compatible_fit`` convention in ``validate_fsl_dr.py``: one input,
|
|
7
|
+
``run_baselines=False``, ``temporal_ddof=1`` (FSL ``des_norm=1``).
|
|
8
|
+
|
|
9
|
+
Only continuous stage-two beta maps and normalized timecourses are calculated.
|
|
10
|
+
The module adds no spatial prior, frame weighting or statistical-Z output.
|
|
11
|
+
Caller-side validation must reject nonfinite or constant masked observations;
|
|
12
|
+
this implementation never discards a voxel or changes the analysis support.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
SOURCE_NAME = "dr_improvements_20261031/dr_core.py"
|
|
22
|
+
SOURCE_SHA256 = "24b366a5e8420b8ecc01f1dca59fe35c8d37f816c3ce53e204ed20e75ffd9052"
|
|
23
|
+
VALIDATOR_NAME = "rest_factorial_pilot/validate_fsl_dr.py"
|
|
24
|
+
VALIDATOR_SHA256 = "0da89b814179a215e085cc81648f77b688d37fa2d41028ea523dd3e2d2dbe15c"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def rank_info(matrix, name):
|
|
28
|
+
"""Use the source core's float64 SVD tolerance and full-column-rank rule."""
|
|
29
|
+
singular = np.linalg.svd(matrix, compute_uv=False)
|
|
30
|
+
tolerance = np.finfo(np.float64).eps * max(matrix.shape) * singular[0]
|
|
31
|
+
rank = int(np.count_nonzero(singular > tolerance))
|
|
32
|
+
if rank != matrix.shape[1]:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
"%s is rank deficient: rank %d, %d columns"
|
|
35
|
+
% (name, rank, matrix.shape[1])
|
|
36
|
+
)
|
|
37
|
+
return {
|
|
38
|
+
"rank": rank,
|
|
39
|
+
"condition_number": float(singular[0] / singular[-1]),
|
|
40
|
+
"singular_values": singular.tolist(),
|
|
41
|
+
"rank_tolerance": float(tolerance),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def fit_dual_regression(data, reference, *, chunk_size=8192):
|
|
46
|
+
"""Fit float64 classic beta maps from Y[time, voxel], S[voxel, component].
|
|
47
|
+
|
|
48
|
+
There is one complete, already prepared fMRI input. Temporal demeaning
|
|
49
|
+
here is an intrinsic regression operation; there is no input variance
|
|
50
|
+
normalization, run partitioning or concatenation.
|
|
51
|
+
"""
|
|
52
|
+
started = time.perf_counter()
|
|
53
|
+
if np.iscomplexobj(data) or np.iscomplexobj(reference):
|
|
54
|
+
raise ValueError("Data and templates must be real-valued")
|
|
55
|
+
data = np.asarray(data)
|
|
56
|
+
reference = np.asarray(reference, dtype=np.float64)
|
|
57
|
+
if data.ndim != 2 or reference.ndim != 2:
|
|
58
|
+
raise ValueError("Expected data [time, voxel] and reference [voxel, component]")
|
|
59
|
+
n_timepoints, n_voxels = data.shape
|
|
60
|
+
n_components = reference.shape[1]
|
|
61
|
+
if reference.shape[0] != n_voxels or n_components < 1:
|
|
62
|
+
raise ValueError("Data and reference have incompatible dimensions")
|
|
63
|
+
if n_voxels <= n_components:
|
|
64
|
+
raise ValueError("Insufficient masked voxels for the spatial template design")
|
|
65
|
+
if n_timepoints <= n_components + 1:
|
|
66
|
+
raise ValueError("Insufficient timepoints for the stage-two timecourse design")
|
|
67
|
+
if (
|
|
68
|
+
isinstance(chunk_size, (bool, np.bool_))
|
|
69
|
+
or not isinstance(chunk_size, (int, np.integer))
|
|
70
|
+
or chunk_size < 1
|
|
71
|
+
):
|
|
72
|
+
raise ValueError("chunk_size must be a positive integer")
|
|
73
|
+
if not np.isfinite(reference).all():
|
|
74
|
+
raise ValueError("Templates contain nonfinite values inside the analysis mask")
|
|
75
|
+
|
|
76
|
+
means = np.mean(reference, axis=0)
|
|
77
|
+
spatial_scales = np.std(reference, axis=0, ddof=0)
|
|
78
|
+
if np.any(spatial_scales <= 0) or not np.isfinite(spatial_scales).all():
|
|
79
|
+
raise ValueError("A template is spatially constant inside the analysis mask")
|
|
80
|
+
matrix = (reference - means) / spatial_scales
|
|
81
|
+
template_rank = rank_info(matrix, "Spatial template design")
|
|
82
|
+
left, singular, right = np.linalg.svd(matrix, full_matrices=False)
|
|
83
|
+
del left
|
|
84
|
+
gram_inverse = (right.T / (singular * singular)) @ right
|
|
85
|
+
|
|
86
|
+
cross = np.zeros((n_timepoints, n_components), dtype=np.float64)
|
|
87
|
+
for start in range(0, n_voxels, chunk_size):
|
|
88
|
+
stop = min(start + chunk_size, n_voxels)
|
|
89
|
+
block = np.array(data[:, start:stop], dtype=np.float64, copy=True)
|
|
90
|
+
if not np.isfinite(block).all():
|
|
91
|
+
raise ValueError("BOLD values are nonfinite inside the analysis mask")
|
|
92
|
+
block -= block.mean(axis=0, keepdims=True)
|
|
93
|
+
cross += block @ matrix[start:stop]
|
|
94
|
+
raw = cross @ gram_inverse
|
|
95
|
+
centered = raw - raw.mean(axis=0, keepdims=True)
|
|
96
|
+
temporal_scales = np.std(centered, axis=0, ddof=1)
|
|
97
|
+
if np.any(temporal_scales <= 0) or not np.isfinite(temporal_scales).all():
|
|
98
|
+
raise ValueError("A stage-one timecourse is constant or invalid")
|
|
99
|
+
timecourses = centered / temporal_scales
|
|
100
|
+
design = np.column_stack((np.ones(n_timepoints), timecourses))
|
|
101
|
+
timecourse_rank = rank_info(design, "Stage-two timecourse design")
|
|
102
|
+
linear = np.linalg.solve(design.T @ design, design.T)
|
|
103
|
+
components = np.empty((n_voxels, n_components), dtype=np.float64)
|
|
104
|
+
for start in range(0, n_voxels, chunk_size):
|
|
105
|
+
stop = min(start + chunk_size, n_voxels)
|
|
106
|
+
block = np.array(data[:, start:stop], dtype=np.float64, copy=True)
|
|
107
|
+
block -= block.mean(axis=0, keepdims=True)
|
|
108
|
+
components[start:stop] = (linear @ block)[1:].T
|
|
109
|
+
if not np.isfinite(components).all() or not np.isfinite(timecourses).all():
|
|
110
|
+
raise FloatingPointError("Classic dual regression produced nonfinite output")
|
|
111
|
+
return {
|
|
112
|
+
"components": components,
|
|
113
|
+
"timecourses": timecourses,
|
|
114
|
+
"metadata": {
|
|
115
|
+
"implementation": "classic_dual_regression_FSL_des_norm_1_convention",
|
|
116
|
+
"source_name": SOURCE_NAME,
|
|
117
|
+
"source_sha256": SOURCE_SHA256,
|
|
118
|
+
"validator_name": VALIDATOR_NAME,
|
|
119
|
+
"validator_sha256": VALIDATOR_SHA256,
|
|
120
|
+
"n_timepoints": n_timepoints,
|
|
121
|
+
"n_voxels": n_voxels,
|
|
122
|
+
"n_components": n_components,
|
|
123
|
+
"spatial_template_centering": True,
|
|
124
|
+
"spatial_template_standardization_ddof": 0,
|
|
125
|
+
"spatial_template_means": means.tolist(),
|
|
126
|
+
"spatial_template_sd": spatial_scales.tolist(),
|
|
127
|
+
"temporal_centering_Y": "whole_single_input_per_voxel",
|
|
128
|
+
"stage1_timecourse_centering": True,
|
|
129
|
+
"temporal_standardization_ddof": 1,
|
|
130
|
+
"stage1_timecourse_sd": temporal_scales.tolist(),
|
|
131
|
+
"timecourses_returned": "centered_sample_SD_normalized_stage1",
|
|
132
|
+
"stage2_intercept": True,
|
|
133
|
+
"n_nuisance": 1,
|
|
134
|
+
"run_baselines": False,
|
|
135
|
+
"des_norm": 1,
|
|
136
|
+
"template_rank_diagnostics": template_rank,
|
|
137
|
+
"timecourse_rank_diagnostics": timecourse_rank,
|
|
138
|
+
"residual_degrees_of_freedom": n_timepoints - n_components - 1,
|
|
139
|
+
"map_definition": "stage2_continuous_beta",
|
|
140
|
+
"output_dtype": "float64",
|
|
141
|
+
"source_beta_storage_dtype": "float32",
|
|
142
|
+
"spatial_prior_added": False,
|
|
143
|
+
"frame_weighting_added": False,
|
|
144
|
+
"fsl_invoked": False,
|
|
145
|
+
"fsl_equivalence_tested_for_this_code": False,
|
|
146
|
+
"elapsed_seconds": time.perf_counter() - started,
|
|
147
|
+
},
|
|
148
|
+
}
|