nimare 0.4.2rc4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. benchmarks/__init__.py +0 -0
  2. benchmarks/bench_cbma.py +57 -0
  3. nimare/__init__.py +45 -0
  4. nimare/_version.py +21 -0
  5. nimare/annotate/__init__.py +21 -0
  6. nimare/annotate/cogat.py +213 -0
  7. nimare/annotate/gclda.py +924 -0
  8. nimare/annotate/lda.py +147 -0
  9. nimare/annotate/text.py +75 -0
  10. nimare/annotate/utils.py +87 -0
  11. nimare/base.py +217 -0
  12. nimare/cli.py +124 -0
  13. nimare/correct.py +462 -0
  14. nimare/dataset.py +685 -0
  15. nimare/decode/__init__.py +33 -0
  16. nimare/decode/base.py +115 -0
  17. nimare/decode/continuous.py +462 -0
  18. nimare/decode/discrete.py +753 -0
  19. nimare/decode/encode.py +110 -0
  20. nimare/decode/utils.py +44 -0
  21. nimare/diagnostics.py +510 -0
  22. nimare/estimator.py +139 -0
  23. nimare/extract/__init__.py +19 -0
  24. nimare/extract/extract.py +466 -0
  25. nimare/extract/utils.py +295 -0
  26. nimare/generate.py +331 -0
  27. nimare/io.py +635 -0
  28. nimare/meta/__init__.py +39 -0
  29. nimare/meta/cbma/__init__.py +6 -0
  30. nimare/meta/cbma/ale.py +951 -0
  31. nimare/meta/cbma/base.py +947 -0
  32. nimare/meta/cbma/mkda.py +1361 -0
  33. nimare/meta/cbmr.py +970 -0
  34. nimare/meta/ibma.py +1683 -0
  35. nimare/meta/kernel.py +501 -0
  36. nimare/meta/models.py +1199 -0
  37. nimare/meta/utils.py +494 -0
  38. nimare/nimads.py +492 -0
  39. nimare/reports/__init__.py +24 -0
  40. nimare/reports/base.py +664 -0
  41. nimare/reports/default.yml +123 -0
  42. nimare/reports/figures.py +651 -0
  43. nimare/reports/report.tpl +160 -0
  44. nimare/resources/__init__.py +1 -0
  45. nimare/resources/atlases/Harvard-Oxford-LICENSE +93 -0
  46. nimare/resources/atlases/HarvardOxford-cort-maxprob-thr25-2mm.nii.gz +0 -0
  47. nimare/resources/database_file_manifest.json +142 -0
  48. nimare/resources/english_spellings.csv +1738 -0
  49. nimare/resources/filenames.json +32 -0
  50. nimare/resources/neurosynth_laird_studies.json +58773 -0
  51. nimare/resources/neurosynth_stoplist.txt +396 -0
  52. nimare/resources/nidm_pain_dset.json +1349 -0
  53. nimare/resources/references.bib +541 -0
  54. nimare/resources/semantic_knowledge_children.txt +325 -0
  55. nimare/resources/semantic_relatedness_children.txt +249 -0
  56. nimare/resources/templates/MNI152_2x2x2_brainmask.nii.gz +0 -0
  57. nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz +0 -0
  58. nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_desc-brain_mask.nii.gz +0 -0
  59. nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_T1w.nii.gz +0 -0
  60. nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_desc-brain_mask.nii.gz +0 -0
  61. nimare/results.py +225 -0
  62. nimare/stats.py +276 -0
  63. nimare/tests/__init__.py +1 -0
  64. nimare/tests/conftest.py +229 -0
  65. nimare/tests/data/amygdala_roi.nii.gz +0 -0
  66. nimare/tests/data/data-neurosynth_version-7_coordinates.tsv.gz +0 -0
  67. nimare/tests/data/data-neurosynth_version-7_metadata.tsv.gz +0 -0
  68. nimare/tests/data/data-neurosynth_version-7_vocab-terms_source-abstract_type-tfidf_features.npz +0 -0
  69. nimare/tests/data/data-neurosynth_version-7_vocab-terms_vocabulary.txt +100 -0
  70. nimare/tests/data/neurosynth_dset.json +2868 -0
  71. nimare/tests/data/neurosynth_laird_studies.json +58773 -0
  72. nimare/tests/data/nidm_pain_dset.json +1349 -0
  73. nimare/tests/data/nimads_annotation.json +1 -0
  74. nimare/tests/data/nimads_studyset.json +1 -0
  75. nimare/tests/data/test_baseline.txt +2 -0
  76. nimare/tests/data/test_pain_dataset.json +1278 -0
  77. nimare/tests/data/test_pain_dataset_multiple_contrasts.json +1242 -0
  78. nimare/tests/data/test_sleuth_file.txt +18 -0
  79. nimare/tests/data/test_sleuth_file2.txt +10 -0
  80. nimare/tests/data/test_sleuth_file3.txt +5 -0
  81. nimare/tests/data/test_sleuth_file4.txt +5 -0
  82. nimare/tests/data/test_sleuth_file5.txt +5 -0
  83. nimare/tests/test_annotate_cogat.py +32 -0
  84. nimare/tests/test_annotate_gclda.py +86 -0
  85. nimare/tests/test_annotate_lda.py +27 -0
  86. nimare/tests/test_dataset.py +99 -0
  87. nimare/tests/test_decode_continuous.py +132 -0
  88. nimare/tests/test_decode_discrete.py +92 -0
  89. nimare/tests/test_diagnostics.py +168 -0
  90. nimare/tests/test_estimator_performance.py +385 -0
  91. nimare/tests/test_extract.py +46 -0
  92. nimare/tests/test_generate.py +247 -0
  93. nimare/tests/test_io.py +240 -0
  94. nimare/tests/test_meta_ale.py +298 -0
  95. nimare/tests/test_meta_cbmr.py +295 -0
  96. nimare/tests/test_meta_ibma.py +240 -0
  97. nimare/tests/test_meta_kernel.py +209 -0
  98. nimare/tests/test_meta_mkda.py +234 -0
  99. nimare/tests/test_nimads.py +21 -0
  100. nimare/tests/test_reports.py +110 -0
  101. nimare/tests/test_stats.py +101 -0
  102. nimare/tests/test_transforms.py +272 -0
  103. nimare/tests/test_utils.py +200 -0
  104. nimare/tests/test_workflows.py +221 -0
  105. nimare/tests/utils.py +126 -0
  106. nimare/transforms.py +907 -0
  107. nimare/utils.py +1367 -0
  108. nimare/workflows/__init__.py +14 -0
  109. nimare/workflows/base.py +189 -0
  110. nimare/workflows/cbma.py +165 -0
  111. nimare/workflows/ibma.py +108 -0
  112. nimare/workflows/macm.py +77 -0
  113. nimare/workflows/misc.py +65 -0
  114. nimare-0.4.2rc4.dist-info/LICENSE +21 -0
  115. nimare-0.4.2rc4.dist-info/METADATA +124 -0
  116. nimare-0.4.2rc4.dist-info/RECORD +119 -0
  117. nimare-0.4.2rc4.dist-info/WHEEL +5 -0
  118. nimare-0.4.2rc4.dist-info/entry_points.txt +2 -0
  119. nimare-0.4.2rc4.dist-info/top_level.txt +2 -0
nimare/meta/utils.py ADDED
@@ -0,0 +1,494 @@
1
+ """Utilities for coordinate-based meta-analysis estimators."""
2
+
3
+ import warnings
4
+
5
+ import numpy as np
6
+ import sparse
7
+ from numba import jit
8
+ from scipy import ndimage
9
+
10
+ from nimare.utils import unique_rows
11
+
12
+
13
+ @jit(nopython=True, cache=True)
14
+ def _convolve_sphere(kernel, ijks, index, max_shape):
15
+ """Convolve peaks with a spherical kernel.
16
+
17
+ Parameters
18
+ ----------
19
+ kernel : 2D numpy.ndarray
20
+ IJK coordinates of a sphere, relative to a central point
21
+ (not the brain template).
22
+ peaks : 2D numpy.ndarray
23
+ The IJK coordinates of peaks to convolve with the kernel.
24
+ max_shape: 1D numpy.ndarray
25
+ The maximum shape of the image volume.
26
+
27
+ Returns
28
+ -------
29
+ sphere_coords : 2D numpy.ndarray
30
+ All coordinates that fall within any sphere.ß∑
31
+ Coordinates from overlapping spheres will appear twice.
32
+ """
33
+
34
+ def np_all_axis1(x):
35
+ """Numba compatible version of np.all(x, axis=1)."""
36
+ out = np.ones(x.shape[0], dtype=np.bool8)
37
+ for i in range(x.shape[1]):
38
+ out = np.logical_and(out, x[:, i])
39
+ return out
40
+
41
+ peaks = ijks[index]
42
+ sphere_coords = np.zeros((kernel.shape[1] * len(peaks), 3), dtype=np.int32)
43
+ chunk_idx = np.arange(0, (kernel.shape[1]), dtype=np.int64)
44
+ for peak in peaks:
45
+ sphere_coords[chunk_idx, :] = kernel.T + peak
46
+ chunk_idx = chunk_idx + kernel.shape[1]
47
+
48
+ # Mask coordinates beyond space
49
+ idx = np_all_axis1(np.logical_and(sphere_coords >= 0, np.less(sphere_coords, max_shape)))
50
+
51
+ return sphere_coords[idx, :]
52
+
53
+
54
+ def compute_kda_ma(
55
+ mask,
56
+ ijks,
57
+ r,
58
+ value=1.0,
59
+ exp_idx=None,
60
+ sum_overlap=False,
61
+ sum_across_studies=False,
62
+ ):
63
+ """Compute (M)KDA modeled activation (MA) map.
64
+
65
+ .. versionchanged:: 0.0.12
66
+
67
+ * Remove low-memory option in favor of sparse arrays.
68
+ * Return 4D sparse array.
69
+ * `shape` and `vox_dims` parameters have been removed. That information is now extracted
70
+ from the new parameter `mask`.
71
+
72
+ .. versionadded:: 0.0.4
73
+
74
+ Replaces the values around each focus in ijk with binary sphere.
75
+
76
+ Parameters
77
+ ----------
78
+ mask : img_like
79
+ Mask to extract the MA maps shape (typically (91, 109, 91)) and voxel dimension.
80
+ The mask is applied the data coordinated before creating the kernel_data.
81
+ ijks : array-like
82
+ Indices of foci. Each row is a coordinate, with the three columns
83
+ corresponding to index in each of three dimensions.
84
+ r : :obj:`int`
85
+ Sphere radius, in mm.
86
+ value : :obj:`int`
87
+ Value for sphere.
88
+ exp_idx : array_like
89
+ Optional indices of experiments. If passed, must be of same length as
90
+ ijks. Each unique value identifies all coordinates in ijk that come from
91
+ the same experiment. If None passed, it is assumed that all coordinates
92
+ come from the same experiment.
93
+ sum_overlap : :obj:`bool`
94
+ Whether to sum voxel values in overlapping spheres.
95
+ sum_across_studies : :obj:`bool`
96
+ Whether to sum voxel values across studies.
97
+
98
+ Returns
99
+ -------
100
+ kernel_data : :obj:`sparse._coo.core.COO`
101
+ 4D sparse array. If `exp_idx` is none, a 3d array in the same
102
+ shape as the `shape` argument is returned. If `exp_idx` is passed, a 4d array
103
+ is returned, where the first dimension has size equal to the number of
104
+ unique experiments, and the remaining 3 dimensions are equal to `shape`.
105
+ """
106
+ if sum_overlap and sum_across_studies:
107
+ raise NotImplementedError("sum_overlap and sum_across_studies cannot both be True.")
108
+
109
+ # recast ijks to int32 to reduce memory footprint
110
+ ijks = ijks.astype(np.int32)
111
+ shape = mask.shape
112
+ vox_dims = mask.header.get_zooms()
113
+
114
+ mask_data = mask.get_fdata().astype(bool)
115
+
116
+ if exp_idx is None:
117
+ exp_idx = np.ones(len(ijks))
118
+
119
+ exp_idx_uniq, exp_idx = np.unique(exp_idx, return_inverse=True)
120
+ n_studies = len(exp_idx_uniq)
121
+
122
+ kernel_shape = (n_studies,) + shape
123
+
124
+ n_dim = ijks.shape[1]
125
+ xx, yy, zz = [slice(-r // vox_dims[i], r // vox_dims[i] + 0.01, 1) for i in range(n_dim)]
126
+ cube = np.vstack([row.ravel() for row in (np.mgrid[xx, yy, zz]).astype(np.int32)])
127
+ kernel = cube[:, np.sum(np.dot(np.diag(vox_dims), cube) ** 2, 0) ** 0.5 <= r]
128
+
129
+ if sum_across_studies:
130
+ all_values = np.zeros(shape, dtype=np.int32)
131
+
132
+ # Loop over experiments
133
+ for i_exp, _ in enumerate(exp_idx_uniq):
134
+ # Index peaks by experiment
135
+ curr_exp_idx = exp_idx == i_exp
136
+ sphere_coords = _convolve_sphere(kernel, ijks, curr_exp_idx, np.array(shape))
137
+
138
+ # preallocate array for current study
139
+ study_values = np.zeros(shape, dtype=np.int32)
140
+ study_values[sphere_coords[:, 0], sphere_coords[:, 1], sphere_coords[:, 2]] = value
141
+
142
+ # Sum across studies
143
+ all_values += study_values
144
+
145
+ # Only return values within the mask
146
+ all_values = all_values.reshape(-1)
147
+ kernel_data = all_values[mask_data.reshape(-1)]
148
+
149
+ else:
150
+ all_coords = []
151
+ # Loop over experiments
152
+ for i_exp, _ in enumerate(exp_idx_uniq):
153
+ curr_exp_idx = exp_idx == i_exp
154
+ # Convolve with sphere
155
+ all_spheres = _convolve_sphere(kernel, ijks, curr_exp_idx, np.array(shape))
156
+
157
+ if not sum_overlap:
158
+ all_spheres = unique_rows(all_spheres)
159
+
160
+ # Apply mask
161
+ sphere_idx_inside_mask = np.where(mask_data[tuple(all_spheres.T)])[0]
162
+ all_spheres = all_spheres[sphere_idx_inside_mask, :]
163
+
164
+ # Combine experiment id with coordinates
165
+ all_coords.append(all_spheres)
166
+
167
+ # Add exp_idx to coordinates
168
+ exp_shapes = [coords.shape[0] for coords in all_coords]
169
+ exp_indicator = np.repeat(np.arange(len(exp_shapes)), exp_shapes)
170
+
171
+ all_coords = np.vstack(all_coords).T
172
+ all_coords = np.insert(all_coords, 0, exp_indicator, axis=0)
173
+
174
+ kernel_data = sparse.COO(
175
+ all_coords, data=value, has_duplicates=sum_overlap, shape=kernel_shape
176
+ )
177
+
178
+ return kernel_data
179
+
180
+
181
+ def compute_ale_ma(mask, ijks, kernel=None, exp_idx=None, sample_sizes=None, use_dict=False):
182
+ """Generate ALE modeled activation (MA) maps.
183
+
184
+ Replaces the values around each focus in ijk with the contrast-specific
185
+ kernel. Takes the element-wise maximum when looping through foci, which
186
+ accounts for foci which are near to one another and may have overlapping
187
+ kernels.
188
+
189
+ .. versionchanged:: 0.0.12
190
+
191
+ * This function now returns a 4D sparse array.
192
+ * `shape` parameter has been removed. That information is now extracted
193
+ from the new parameter `mask`.
194
+ * Replace `ijk` with `ijks`.
195
+ * New parameters: `exp_idx`, `sample_sizes`, and `use_dict`.
196
+
197
+ Parameters
198
+ ----------
199
+ mask : img_like
200
+ Mask to extract the MA maps shape (typically (91, 109, 91)) and voxel dimension.
201
+ The mask is applied to the coordinates before creating the kernel_data.
202
+ ijks : array-like
203
+ Indices of foci. Each row is a coordinate, with the three columns
204
+ corresponding to index in each of three dimensions.
205
+ kernel : array-like, or None, optional
206
+ 3D array of smoothing kernel. Typically of shape (30, 30, 30).
207
+ exp_idx : array_like
208
+ Optional indices of experiments. If passed, must be of same length as
209
+ ijks. Each unique value identifies all coordinates in ijk that come from
210
+ the same experiment. If None passed, it is assumed that all coordinates
211
+ come from the same experiment.
212
+ sample_sizes : array_like, :obj:`int` or None, optional
213
+ Array of smaple sizes or sample size, used to derive FWHM for Gaussian kernel.
214
+ use_dict : :obj:`bool`, optional
215
+ If True, empty kernels dictionary is used to retain the kernel for each element of
216
+ sample_sizes. If False and sample_sizes is int, the ale kernel is calculated for
217
+ sample_sizes. If False and sample_sizes is None, the unique kernels is used.
218
+
219
+ Returns
220
+ -------
221
+ kernel_data : :obj:`sparse._coo.core.COO`
222
+ 4D sparse array. If `exp_idx` is none, a 3d array in the same
223
+ shape as the `shape` argument is returned. If `exp_idx` is passed, a 4d array
224
+ is returned, where the first dimension has size equal to the number of
225
+ unique experiments, and the remaining 3 dimensions are equal to `shape`.
226
+ """
227
+ if use_dict:
228
+ if kernel is not None:
229
+ warnings.warn("The kernel provided will be replace by an empty dictionary.")
230
+ kernels = {} # retain kernels in dictionary to speed things up
231
+ if not isinstance(sample_sizes, np.ndarray):
232
+ raise ValueError("To use a kernel dictionary sample_sizes must be a list.")
233
+ elif sample_sizes is not None:
234
+ if not isinstance(sample_sizes, int):
235
+ raise ValueError("If use_dict is False, sample_sizes provided must be integer.")
236
+ else:
237
+ if kernel is None:
238
+ raise ValueError("3D array of smoothing kernel must be provided.")
239
+
240
+ if exp_idx is None:
241
+ exp_idx = np.ones(len(ijks))
242
+
243
+ shape = mask.shape
244
+ mask_data = mask.get_fdata().astype(bool)
245
+
246
+ exp_idx_uniq, exp_idx = np.unique(exp_idx, return_inverse=True)
247
+ n_studies = len(exp_idx_uniq)
248
+
249
+ kernel_shape = (n_studies,) + shape
250
+ all_exp = []
251
+ all_coords = []
252
+ all_data = []
253
+ for i_exp, _ in enumerate(exp_idx_uniq):
254
+ # Index peaks by experiment
255
+ curr_exp_idx = exp_idx == i_exp
256
+ ijk = ijks[curr_exp_idx]
257
+
258
+ if use_dict:
259
+ # Get sample_size from input
260
+ sample_size = sample_sizes[curr_exp_idx][0]
261
+ if sample_size not in kernels.keys():
262
+ _, kernel = get_ale_kernel(mask, sample_size=sample_size)
263
+ kernels[sample_size] = kernel
264
+ else:
265
+ kernel = kernels[sample_size]
266
+ elif sample_sizes is not None:
267
+ _, kernel = get_ale_kernel(mask, sample_size=sample_sizes)
268
+
269
+ mid = int(np.floor(kernel.shape[0] / 2.0))
270
+ mid1 = mid + 1
271
+ ma_values = np.zeros(shape)
272
+ for j_peak in range(ijk.shape[0]):
273
+ i, j, k = ijk[j_peak, :]
274
+ xl = max(i - mid, 0)
275
+ xh = min(i + mid1, ma_values.shape[0])
276
+ yl = max(j - mid, 0)
277
+ yh = min(j + mid1, ma_values.shape[1])
278
+ zl = max(k - mid, 0)
279
+ zh = min(k + mid1, ma_values.shape[2])
280
+ xlk = mid - (i - xl)
281
+ xhk = mid - (i - xh)
282
+ ylk = mid - (j - yl)
283
+ yhk = mid - (j - yh)
284
+ zlk = mid - (k - zl)
285
+ zhk = mid - (k - zh)
286
+
287
+ if (
288
+ (xl >= 0)
289
+ & (xh >= 0)
290
+ & (yl >= 0)
291
+ & (yh >= 0)
292
+ & (zl >= 0)
293
+ & (zh >= 0)
294
+ & (xlk >= 0)
295
+ & (xhk >= 0)
296
+ & (ylk >= 0)
297
+ & (yhk >= 0)
298
+ & (zlk >= 0)
299
+ & (zhk >= 0)
300
+ ):
301
+ ma_values[xl:xh, yl:yh, zl:zh] = np.maximum(
302
+ ma_values[xl:xh, yl:yh, zl:zh], kernel[xlk:xhk, ylk:yhk, zlk:zhk]
303
+ )
304
+ # Set voxel outside the mask to zero.
305
+ ma_values[~mask_data] = 0
306
+ nonzero_idx = np.where(ma_values > 0)
307
+
308
+ all_exp.append(np.full(nonzero_idx[0].shape[0], i_exp))
309
+ all_coords.append(np.vstack(nonzero_idx))
310
+ all_data.append(ma_values[nonzero_idx])
311
+
312
+ exp = np.hstack(all_exp)
313
+ coords = np.vstack((exp.flatten(), np.hstack(all_coords)))
314
+ data = np.hstack(all_data).flatten()
315
+
316
+ kernel_data = sparse.COO(coords, data, shape=kernel_shape)
317
+
318
+ return kernel_data
319
+
320
+
321
+ def get_ale_kernel(img, sample_size=None, fwhm=None):
322
+ """Estimate 3D Gaussian and sigma (in voxels) for ALE kernel given sample size or fwhm."""
323
+ if sample_size is not None and fwhm is not None:
324
+ raise ValueError('Only one of "sample_size" and "fwhm" may be specified')
325
+ elif sample_size is None and fwhm is None:
326
+ raise ValueError('Either "sample_size" or "fwhm" must be provided')
327
+ elif sample_size is not None:
328
+ uncertain_templates = (
329
+ 5.7 / (2.0 * np.sqrt(2.0 / np.pi)) * np.sqrt(8.0 * np.log(2.0))
330
+ ) # pylint: disable=no-member
331
+ # Assuming 11.6 mm ED between matching points
332
+ uncertain_subjects = (11.6 / (2 * np.sqrt(2 / np.pi)) * np.sqrt(8 * np.log(2))) / np.sqrt(
333
+ sample_size
334
+ ) # pylint: disable=no-member
335
+ fwhm = np.sqrt(uncertain_subjects**2 + uncertain_templates**2)
336
+
337
+ fwhm_vox = fwhm / np.sqrt(np.prod(img.header.get_zooms()))
338
+ sigma_vox = (
339
+ fwhm_vox * np.sqrt(2.0) / (np.sqrt(2.0 * np.log(2.0)) * 2.0)
340
+ ) # pylint: disable=no-member
341
+
342
+ data = np.zeros((31, 31, 31))
343
+ mid = int(np.floor(data.shape[0] / 2.0))
344
+ data[mid, mid, mid] = 1.0
345
+ kernel = ndimage.gaussian_filter(data, sigma_vox, mode="constant")
346
+
347
+ # Crop kernel to drop surrounding zeros
348
+ mn = np.min(np.where(kernel > np.spacing(1))[0])
349
+ mx = np.max(np.where(kernel > np.spacing(1))[0])
350
+ kernel = kernel[mn : mx + 1, mn : mx + 1, mn : mx + 1]
351
+ mid = int(np.floor(data.shape[0] / 2.0))
352
+ return sigma_vox, kernel
353
+
354
+
355
+ def _get_last_bin(arr1d):
356
+ """Index the last location in a 1D array with a non-zero value."""
357
+ if np.any(arr1d):
358
+ last_bin = np.where(arr1d)[0][-1]
359
+
360
+ else:
361
+ last_bin = 0
362
+
363
+ return last_bin
364
+
365
+
366
+ def _calculate_cluster_measures(arr3d, threshold, conn, tail="upper"):
367
+ """Calculate maximum cluster mass and size for an array.
368
+
369
+ This method assesses both positive and negative clusters.
370
+
371
+ Parameters
372
+ ----------
373
+ arr3d : :obj:`numpy.ndarray`
374
+ Unthresholded 3D summary-statistic matrix. This matrix will end up changed in place.
375
+ threshold : :obj:`float`
376
+ Uncorrected summary-statistic thresholded for defining clusters.
377
+ conn : :obj:`numpy.ndarray` of shape (3, 3, 3)
378
+ Connectivity matrix for defining clusters.
379
+
380
+ Returns
381
+ -------
382
+ max_size, max_mass : :obj:`float`
383
+ Maximum cluster size and mass from the matrix.
384
+ """
385
+ if tail == "upper":
386
+ arr3d[arr3d <= threshold] = 0
387
+ else:
388
+ arr3d[np.abs(arr3d) <= threshold] = 0
389
+
390
+ labeled_arr3d = np.empty(arr3d.shape, int)
391
+ labeled_arr3d, _ = ndimage.label(arr3d > 0, conn)
392
+
393
+ if tail == "two":
394
+ # Label positive and negative clusters separately
395
+ n_positive_clusters = np.max(labeled_arr3d)
396
+ temp_labeled_arr3d, _ = ndimage.label(arr3d < 0, conn)
397
+ temp_labeled_arr3d[temp_labeled_arr3d > 0] += n_positive_clusters
398
+ labeled_arr3d = labeled_arr3d + temp_labeled_arr3d
399
+ del temp_labeled_arr3d
400
+
401
+ clust_sizes = np.bincount(labeled_arr3d.flatten())
402
+ clust_vals = np.arange(0, clust_sizes.shape[0])
403
+
404
+ # Cluster mass-based inference
405
+ max_mass = 0
406
+ for unique_val in clust_vals[1:]:
407
+ ss_vals = np.abs(arr3d[labeled_arr3d == unique_val]) - threshold
408
+ max_mass = np.maximum(max_mass, np.sum(ss_vals))
409
+
410
+ # Cluster size-based inference
411
+ clust_sizes = clust_sizes[1:] # First cluster is zeros in matrix
412
+ if clust_sizes.size:
413
+ max_size = np.max(clust_sizes)
414
+ else:
415
+ max_size = 0
416
+
417
+ return max_size, max_mass
418
+
419
+
420
+ @jit(nopython=True, cache=True)
421
+ def _apply_liberal_mask(data):
422
+ """Separate input image data in bags of voxels that have a valid value across the same studies.
423
+
424
+ Parameters
425
+ ----------
426
+ data : (S x V) :class:`numpy.ndarray`
427
+ 2D numpy array (S x V) of images, where S is study and V is voxel.
428
+
429
+ Returns
430
+ -------
431
+ values_lst : :obj:`list` of :obj:`numpy.ndarray`
432
+ List of 2D numpy arrays (s x v) of images, where the voxel v have a valid
433
+ value in study s.
434
+ voxel_mask_lst : :obj:`list` of :obj:`numpy.ndarray`
435
+ List of 1D numpy arrays (v) of voxel indices for the corresponding bag.
436
+ study_mask_lst : :obj:`list` of :obj:`numpy.ndarray`
437
+ List of 1D numpy arrays (s) of study indices for the corresponding bag.
438
+
439
+ Notes
440
+ -----
441
+ Parts of the function are implemented with nested for loops to
442
+ improve the speed with the numba compiler.
443
+
444
+ """
445
+ MIN_STUDY_THRESH = 2
446
+
447
+ n_voxels = data.shape[1]
448
+ # Get indices of non-nan and zero value of studies for each voxel
449
+ mask = ~np.isnan(data) & (data != 0)
450
+ study_by_voxels_idxs = [np.where(mask[:, i])[0] for i in range(n_voxels)]
451
+
452
+ # Group studies by the same number of non-nan voxels
453
+ matches = []
454
+ all_indices = []
455
+ for col_i in range(n_voxels):
456
+ if col_i in all_indices:
457
+ continue
458
+
459
+ vox_match = [col_i]
460
+ all_indices.append(col_i)
461
+ for col_j in range(col_i + 1, n_voxels):
462
+ if (
463
+ len(study_by_voxels_idxs[col_i]) == len(study_by_voxels_idxs[col_j])
464
+ and np.array_equal(study_by_voxels_idxs[col_i], study_by_voxels_idxs[col_j])
465
+ and col_j not in all_indices
466
+ ):
467
+ vox_match.append(col_j)
468
+ all_indices.append(col_j)
469
+
470
+ matches.append(np.array(vox_match))
471
+
472
+ values_lst, voxel_mask_lst, study_mask_lst = [], [], []
473
+ for voxel_mask in matches:
474
+ n_masked_voxels = len(voxel_mask)
475
+ # This is the same for all voxels in the match
476
+ study_mask = study_by_voxels_idxs[voxel_mask[0]]
477
+
478
+ if len(study_mask) < MIN_STUDY_THRESH:
479
+ # TODO: Figure out how raise a warning in numba
480
+ # warnings.warn(
481
+ # f"Removing voxels: {voxel_mask} from the analysis. Not present in 2+ studies."
482
+ # )
483
+ continue
484
+
485
+ values = np.zeros((len(study_mask), n_masked_voxels))
486
+ for vox_i, vox in enumerate(voxel_mask):
487
+ for std_i, study in enumerate(study_mask):
488
+ values[std_i, vox_i] = data[study, vox]
489
+
490
+ values_lst.append(values)
491
+ voxel_mask_lst.append(voxel_mask)
492
+ study_mask_lst.append(study_mask)
493
+
494
+ return values_lst, voxel_mask_lst, study_mask_lst