nltools 0.6.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.
Files changed (95) hide show
  1. nltools/__init__.py +55 -0
  2. nltools/algorithms/__init__.py +90 -0
  3. nltools/algorithms/alignment/__init__.py +21 -0
  4. nltools/algorithms/alignment/procrustes.py +565 -0
  5. nltools/algorithms/alignment/srm.py +758 -0
  6. nltools/algorithms/backends.py +1059 -0
  7. nltools/algorithms/corrections.py +177 -0
  8. nltools/algorithms/decoding.py +327 -0
  9. nltools/algorithms/inference/__init__.py +50 -0
  10. nltools/algorithms/inference/bootstrap.py +1386 -0
  11. nltools/algorithms/inference/correlation.py +373 -0
  12. nltools/algorithms/inference/intersubject.py +422 -0
  13. nltools/algorithms/inference/isc.py +1554 -0
  14. nltools/algorithms/inference/matrix.py +602 -0
  15. nltools/algorithms/inference/one_sample.py +288 -0
  16. nltools/algorithms/inference/random.py +122 -0
  17. nltools/algorithms/inference/timeseries.py +347 -0
  18. nltools/algorithms/inference/two_sample.py +212 -0
  19. nltools/algorithms/inference/utils.py +58 -0
  20. nltools/algorithms/inference/validation.py +282 -0
  21. nltools/algorithms/neighborhoods.py +207 -0
  22. nltools/algorithms/outliers.py +308 -0
  23. nltools/algorithms/regression.py +83 -0
  24. nltools/algorithms/signal.py +303 -0
  25. nltools/algorithms/similarity.py +234 -0
  26. nltools/algorithms/validation.py +151 -0
  27. nltools/cross_validation.py +72 -0
  28. nltools/data/__init__.py +30 -0
  29. nltools/data/adjacency/__init__.py +875 -0
  30. nltools/data/adjacency/io.py +111 -0
  31. nltools/data/adjacency/modeling.py +569 -0
  32. nltools/data/adjacency/plotting.py +174 -0
  33. nltools/data/adjacency/state.py +349 -0
  34. nltools/data/adjacency/stats.py +596 -0
  35. nltools/data/adjacency/utils.py +79 -0
  36. nltools/data/atlases/__init__.py +23 -0
  37. nltools/data/atlases/labeling.py +158 -0
  38. nltools/data/atlases/loading.py +76 -0
  39. nltools/data/atlases/registry.py +96 -0
  40. nltools/data/atlases/reporting.py +456 -0
  41. nltools/data/braindata/__init__.py +2170 -0
  42. nltools/data/braindata/analysis.py +1381 -0
  43. nltools/data/braindata/bootstrap.py +398 -0
  44. nltools/data/braindata/io.py +896 -0
  45. nltools/data/braindata/modeling.py +594 -0
  46. nltools/data/braindata/plotting.py +501 -0
  47. nltools/data/braindata/prediction.py +1250 -0
  48. nltools/data/braindata/utils.py +348 -0
  49. nltools/data/braindata/validation.py +197 -0
  50. nltools/data/braindata/viewer.js +266 -0
  51. nltools/data/braindata/viewer.py +770 -0
  52. nltools/data/combine.py +27 -0
  53. nltools/data/designmatrix/__init__.py +1032 -0
  54. nltools/data/designmatrix/append.py +518 -0
  55. nltools/data/designmatrix/diagnostics.py +248 -0
  56. nltools/data/designmatrix/io.py +356 -0
  57. nltools/data/designmatrix/plotting.py +291 -0
  58. nltools/data/designmatrix/regressors.py +463 -0
  59. nltools/data/designmatrix/transforms.py +200 -0
  60. nltools/data/designmatrix/utils.py +350 -0
  61. nltools/data/ownership.py +129 -0
  62. nltools/data/results.py +291 -0
  63. nltools/data/roc/__init__.py +398 -0
  64. nltools/data/simulator/__init__.py +927 -0
  65. nltools/data/simulator/haxby.py +124 -0
  66. nltools/data/validation.py +83 -0
  67. nltools/datasets.py +218 -0
  68. nltools/io/__init__.py +10 -0
  69. nltools/io/events.py +67 -0
  70. nltools/io/h5.py +246 -0
  71. nltools/mask.py +403 -0
  72. nltools/models/__init__.py +11 -0
  73. nltools/models/glm.py +543 -0
  74. nltools/models/results.py +49 -0
  75. nltools/models/ridge.py +1303 -0
  76. nltools/models/validation.py +26 -0
  77. nltools/plotting/__init__.py +32 -0
  78. nltools/plotting/adjacency.py +421 -0
  79. nltools/plotting/brain.py +669 -0
  80. nltools/plotting/decomposition.py +111 -0
  81. nltools/plotting/prediction.py +110 -0
  82. nltools/resources/covariates_example.csv +161 -0
  83. nltools/resources/onsets_example.csv +40 -0
  84. nltools/templates/__init__.py +51 -0
  85. nltools/templates/config.py +144 -0
  86. nltools/templates/fetch.py +260 -0
  87. nltools/templates/matching.py +183 -0
  88. nltools/templates/paths.py +106 -0
  89. nltools/templates/registry.py +25 -0
  90. nltools/utils.py +230 -0
  91. nltools/version.py +13 -0
  92. nltools-0.6.0.dev0.dist-info/METADATA +95 -0
  93. nltools-0.6.0.dev0.dist-info/RECORD +95 -0
  94. nltools-0.6.0.dev0.dist-info/WHEEL +4 -0
  95. nltools-0.6.0.dev0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,422 @@
1
+ """Intersubject correlation, functional connectivity, and phase synchrony."""
2
+
3
+ import numpy as np
4
+ import polars as pl
5
+ from scipy.signal import hilbert
6
+
7
+ from .isc import _isc_permutation_test
8
+ from .matrix import _compute_cross_correlation
9
+ from .utils import _maybe_tqdm
10
+
11
+ from ..signal import (
12
+ _butter_bandpass_filter,
13
+ _phase_mean_angle,
14
+ _phase_rayleigh_p,
15
+ _phase_vector_length,
16
+ )
17
+
18
+
19
+ def _as_ndarray(data, name="data"):
20
+ """Coerce a numpy array, polars DataFrame, or pandas DataFrame to a numpy array."""
21
+ if isinstance(data, np.ndarray):
22
+ return data
23
+ if isinstance(data, pl.DataFrame):
24
+ return data.to_numpy()
25
+ try:
26
+ import pandas as pd
27
+ except ImportError:
28
+ pd = None
29
+ if pd is not None and isinstance(data, pd.DataFrame):
30
+ return data.values
31
+ raise ValueError(
32
+ f"{name} must be a numpy array, polars DataFrame, or pandas DataFrame"
33
+ )
34
+
35
+
36
+ def isc(
37
+ data,
38
+ *,
39
+ n_samples=5000,
40
+ summary="median",
41
+ summary_statistic="pairwise",
42
+ method="bootstrap",
43
+ ci_percentile=95,
44
+ exclude_self_corr=True,
45
+ tail=2,
46
+ metric="correlation",
47
+ return_null=False,
48
+ n_jobs=-1,
49
+ random_state=None,
50
+ progress_bar=False,
51
+ ):
52
+ """Compute intersubject correlation across the subject axis of an aligned array.
53
+
54
+ ISC is summarized with the median, as Chen et al. (2016) recommend;
55
+ `summary='mean'` instead averages after the Fisher r-to-z transform and
56
+ converts back, which avoids inflating the estimate.
57
+
58
+ Three null distributions are available. The default subject-wise bootstrap
59
+ (Chen et al., 2016) resamples subjects with replacement and recomputes the
60
+ chosen summary statistic — for `'pairwise'`, the similarity matrix, where a
61
+ subject drawn twice correlates perfectly with itself, so those entries are
62
+ set to NaN when `exclude_self_corr=True`. P-values use the percentile
63
+ method, as in Brainiak. The classic surrogate methods instead circle-shift
64
+ or phase-randomize each time series (Lancaster et al., 2018), preserving
65
+ its temporal autocorrelation, and recompute ISC.
66
+
67
+ Args:
68
+ data (np.ndarray | pl.DataFrame | pd.DataFrame): Observations by
69
+ subjects, shape `(n_observations, n_subjects)`, with ISC computed
70
+ across the columns; or `(n_observations, n_subjects, n_voxels)` for
71
+ a per-voxel result, in which case `'isc'`, `'p'` and both `'ci'`
72
+ bounds are arrays of length `n_voxels`. DataFrame inputs are 2D
73
+ only.
74
+ n_samples (int): Number of bootstrap draws or surrogate permutations.
75
+ Defaults to 5000.
76
+ summary (str): `'median'` (default) or `'mean'`.
77
+ summary_statistic (str): Which cross-subject comparison to summarize.
78
+ `'pairwise'` (default) correlates every pair of subjects,
79
+ `O(n_subjects²)`; `'leave-one-out'` correlates each subject with
80
+ the mean of the others, `O(n_subjects)`. Leave-one-out gives
81
+ systematically larger values because the averaged reference is less
82
+ noisy than a single subject (Chen et al., 2016, Figure 3).
83
+ method (str): `'bootstrap'` (default), `'circle_shift'`, or
84
+ `'phase_randomize'`.
85
+ ci_percentile (int): Confidence-interval width in percent. Defaults to 95.
86
+ exclude_self_corr (bool): Set self-correlations (the same subject
87
+ bootstrapped twice) to NaN. Applies to the pairwise statistic only.
88
+ Defaults to True.
89
+ tail (int | str): `2` or `'two'` (two-tailed, default) or `1` or `'one'`
90
+ (one-tailed, ISC > 0).
91
+ metric (str): Pairwise similarity metric; any metric accepted by
92
+ sklearn's `pairwise_distances`. Applies to the pairwise statistic
93
+ only. Defaults to `'correlation'`.
94
+ return_null (bool): Include the null distribution in the result.
95
+ Defaults to False.
96
+ n_jobs (int): CPU workers for the resamples; -1 (default) picks the
97
+ count from available memory.
98
+ random_state (int | np.random.RandomState | None): Seed or generator for
99
+ the resampling.
100
+ progress_bar (bool): Display a progress bar. Defaults to False.
101
+
102
+ Returns:
103
+ dict: Keys `'isc'` (observed ISC), `'p'`, and `'ci'` (tuple
104
+ `(lower, upper)`) — floats for 2D data, arrays of length
105
+ `n_voxels` for 3D — and, when `return_null=True`, `'null_dist'`
106
+ (np.ndarray).
107
+
108
+ Note:
109
+ `exclude_self_corr=True` (the default) sets a subject's correlation
110
+ with itself to NaN when the bootstrap draws that subject twice;
111
+ turning it off inflates ISC. Resamples are counted with `n_samples`
112
+ here, for the surrogate methods as well as the bootstrap —
113
+ `n_permute` belongs to the permutation tests in `nltools.algorithms`
114
+ and is not accepted by this function.
115
+
116
+ References:
117
+ Chen, G., Shin, Y. W., Taylor, P. A., Glen, D. R., Reynolds, R. C.,
118
+ Israel, R. B., & Cox, R. W. (2016). Untangling the relatedness among
119
+ correlations, part I: nonparametric approaches to inter-subject
120
+ correlation analysis at the group level. NeuroImage, 142, 248-259.
121
+
122
+ Hall, P., & Wilson, S. R. (1991). Two guidelines for bootstrap
123
+ hypothesis testing. Biometrics, 757-762.
124
+
125
+ Lancaster, G., Iatsenko, D., Pidde, A., Ticcinelli, V., & Stefanovska,
126
+ A. (2018). Surrogate data for hypothesis testing of physical systems.
127
+ Physics Reports, 748, 1-60.
128
+ """
129
+ data = _as_ndarray(data)
130
+
131
+ if summary not in ["mean", "median"]:
132
+ raise ValueError("summary must be ['mean', 'median']")
133
+
134
+ # The engine speaks the same canonical vocabulary (summary=, metric=), so
135
+ # this wrapper only maps n_samples -> n_permute.
136
+ return _isc_permutation_test(
137
+ data,
138
+ n_permute=n_samples, # Map n_samples -> n_permute
139
+ summary=summary,
140
+ summary_statistic=summary_statistic,
141
+ method=method,
142
+ ci_percentile=ci_percentile,
143
+ tail=tail,
144
+ n_jobs=n_jobs,
145
+ random_state=random_state,
146
+ return_null=return_null,
147
+ exclude_self_corr=exclude_self_corr,
148
+ metric=metric,
149
+ progress_bar=progress_bar,
150
+ )
151
+
152
+
153
+ def isc_group(
154
+ group1,
155
+ group2,
156
+ *,
157
+ n_samples=5000,
158
+ summary="median",
159
+ method="permute",
160
+ ci_percentile=95,
161
+ exclude_self_corr=True,
162
+ return_null=False,
163
+ tail=2,
164
+ metric="correlation",
165
+ n_jobs=-1,
166
+ random_state=None,
167
+ progress_bar=False,
168
+ ):
169
+ """Test the difference in pairwise intersubject correlation between two groups.
170
+
171
+ ISC within each group is summarized with the median, as Chen et al. (2016)
172
+ recommend (`summary='mean'` averages after the Fisher r-to-z transform), and
173
+ the observed statistic is `group1 - group2`.
174
+
175
+ Two null distributions are available. The default subject-wise permutation
176
+ (Chen et al., 2016) pools the subjects, computes pairwise similarity within
177
+ and between groups, then reshuffles the group labels and recomputes the
178
+ difference. The subject-wise bootstrap instead resamples subjects with
179
+ replacement within each group; a subject drawn twice correlates perfectly
180
+ with itself, so those entries are set to NaN when `exclude_self_corr=True`.
181
+ P-values use the percentile method (Hall & Wilson, 1991).
182
+
183
+ Runs on plain arrays; `_isc_group_permutation_test` exposes the same engine
184
+ with leave-one-out ISC.
185
+
186
+ Args:
187
+ group1 (np.ndarray | pl.DataFrame | pd.DataFrame): Observations by
188
+ subjects for the first group.
189
+ group2 (np.ndarray | pl.DataFrame | pd.DataFrame): Observations by
190
+ subjects for the second group (same number of observations).
191
+ n_samples (int): Number of permutations or bootstrap draws. Defaults to
192
+ 5000.
193
+ summary (str): `'median'` (default) or `'mean'`.
194
+ method (str): `'permute'` (default) or `'bootstrap'`.
195
+ ci_percentile (float): Confidence-interval width in percent. Defaults to
196
+ 95.
197
+ exclude_self_corr (bool): In the bootstrap, set self-correlations to NaN.
198
+ Defaults to True.
199
+ return_null (bool): Include the null distribution in the result.
200
+ Defaults to False.
201
+ tail (int | str): `2` or `'two'` (two-tailed, default) or `1` or `'one'`
202
+ (one-tailed, group1 > group2).
203
+ metric (str): Pairwise similarity metric; any metric accepted by
204
+ sklearn's `pairwise_distances`. Defaults to `'correlation'`.
205
+ n_jobs (int): CPU workers for the resamples; -1 (default) picks the
206
+ count from available memory.
207
+ random_state (int | np.random.RandomState | None): Random seed for
208
+ reproducibility.
209
+ progress_bar (bool): Display a progress bar. Defaults to False.
210
+
211
+ Returns:
212
+ dict: Keys `'isc_group_difference'` (float, observed difference), `'p'`
213
+ (float), `'ci'` (tuple `(lower, upper)`), and — when
214
+ `return_null=True` — `'null_dist'` (np.ndarray).
215
+
216
+ References:
217
+ Chen, G., Shin, Y. W., Taylor, P. A., Glen, D. R., Reynolds, R. C.,
218
+ Israel, R. B., & Cox, R. W. (2016). Untangling the relatedness among
219
+ correlations, part I: nonparametric approaches to inter-subject
220
+ correlation analysis at the group level. NeuroImage, 142, 248-259.
221
+
222
+ Hall, P., & Wilson, S. R. (1991). Two guidelines for bootstrap
223
+ hypothesis testing. Biometrics, 757-762.
224
+ """
225
+ from .isc import _isc_group_permutation_test
226
+
227
+ group1 = _as_ndarray(group1, name="group1")
228
+ group2 = _as_ndarray(group2, name="group2")
229
+
230
+ if summary not in ["mean", "median"]:
231
+ raise ValueError("summary must be ['mean', 'median']")
232
+
233
+ if group1.shape[0] != group2.shape[0]:
234
+ raise ValueError("group1 has a different number of observations from group2.")
235
+
236
+ if method not in ["permute", "bootstrap"]:
237
+ raise NotImplementedError("method can only be ['permute', 'bootstrap']")
238
+
239
+ # The engine speaks the same canonical vocabulary; only n_samples ->
240
+ # n_permute is mapped here.
241
+ return _isc_group_permutation_test(
242
+ group1,
243
+ group2,
244
+ n_permute=n_samples, # Map parameter name
245
+ summary=summary,
246
+ method=method,
247
+ ci_percentile=ci_percentile,
248
+ tail=tail,
249
+ metric=metric,
250
+ n_jobs=n_jobs,
251
+ random_state=random_state,
252
+ return_null=return_null,
253
+ exclude_self_corr=exclude_self_corr,
254
+ progress_bar=progress_bar,
255
+ summary_statistic="pairwise", # Match old behavior (always pairwise)
256
+ )
257
+
258
+
259
+ def isfc(data, *, method="average", n_jobs=-1, random_state=None, progress_bar=False):
260
+ """Compute intersubject functional connectivity (ISFC) from per-subject matrices.
261
+
262
+ Uses the leave-one-out approach of Simony et al. (2016): for each subject,
263
+ average the other subjects' data and correlate every voxel/ROI time series
264
+ of the target subject with every voxel/ROI time series of that average.
265
+ Subjects are independent, so they are processed in parallel with joblib
266
+ unless `n_jobs=1`.
267
+
268
+ Args:
269
+ data (list[np.ndarray]): One matrix per subject, each
270
+ `(n_observations, n_features)` with identical shapes.
271
+ method (str): Only `'average'` (leave-one-out) is implemented.
272
+ n_jobs (int): Parallel workers; -1 (default) uses all cores, 1 runs
273
+ serially.
274
+ random_state (int | np.random.RandomState | None): Unused. ISFC's
275
+ leave-one-out computation is deterministic and draws no random
276
+ samples; the parameter exists for signature parity with the rest
277
+ of the ISC family (`isc`, `isc_group`).
278
+ progress_bar (bool): Display a progress bar over subjects. Defaults to
279
+ False.
280
+
281
+ Returns:
282
+ list[np.ndarray]: One `(n_features, n_features)` ISFC matrix per
283
+ subject.
284
+
285
+ References:
286
+ Simony, E., Honey, C. J., Chen, J., Lositsky, O., Yeshurun, Y., Wiesel,
287
+ A., & Hasson, U. (2016). Dynamic reconfiguration of the default mode
288
+ network during narrative comprehension. Nature Communications, 7, 12141.
289
+ """
290
+ if method != "average":
291
+ raise NotImplementedError(
292
+ "Only average method is implemented. Pairwise will be added at some point."
293
+ )
294
+
295
+ # Convert to numpy arrays if needed (for efficiency)
296
+ data_arrays = [np.asarray(subject_data) for subject_data in data]
297
+ n_subjects = len(data_arrays)
298
+ subjects = np.arange(n_subjects)
299
+
300
+ # Validate all subjects have same shape
301
+ reference_shape = data_arrays[0].shape
302
+ for i, subject_data in enumerate(data_arrays):
303
+ if subject_data.shape != reference_shape:
304
+ raise ValueError(
305
+ f"All subject matrices must have the same shape. "
306
+ f"Subject 0 has shape {reference_shape}, subject {i} has shape {subject_data.shape}"
307
+ )
308
+
309
+ progress_kwargs = {
310
+ "progress_bar": progress_bar,
311
+ "desc": "ISFC subjects",
312
+ "unit": "subject",
313
+ }
314
+
315
+ if n_jobs == 1:
316
+ # Serial execution (for explicit serial control)
317
+ sub_isfc = []
318
+ for target in _maybe_tqdm(subjects, **progress_kwargs):
319
+ m1 = data_arrays[target]
320
+ sub_mean = np.zeros(m1.shape)
321
+ for y in (y for y in subjects if y != target):
322
+ sub_mean += data_arrays[y]
323
+ # Use inference module function for cross-correlation computation
324
+ sub_isfc.append(_compute_cross_correlation(m1, sub_mean / (n_subjects - 1)))
325
+ else:
326
+ # Parallel execution using joblib (default: n_jobs=-1 uses all cores)
327
+ from joblib import Parallel, delayed
328
+
329
+ def _compute_one_subject_isfc(target_idx):
330
+ """Compute ISFC for one subject (worker function)."""
331
+ m1 = data_arrays[target_idx]
332
+ sub_mean = np.zeros(m1.shape, dtype=m1.dtype)
333
+ for y in (y for y in subjects if y != target_idx):
334
+ sub_mean += data_arrays[y]
335
+ return _compute_cross_correlation(m1, sub_mean / (n_subjects - 1))
336
+
337
+ # Parallelize across subjects
338
+ sub_isfc = Parallel(n_jobs=n_jobs)(
339
+ delayed(_compute_one_subject_isfc)(target)
340
+ for target in _maybe_tqdm(subjects, **progress_kwargs)
341
+ )
342
+
343
+ return sub_isfc
344
+
345
+
346
+ def isps(
347
+ data, *, sampling_freq=0.5, low_cut=0.04, high_cut=0.07, order=5, pairwise=False
348
+ ):
349
+ """Compute dynamic intersubject phase synchrony (ISPS) from an observations-by-subjects array.
350
+
351
+ Instantaneous phase synchrony across subjects for a single voxel/ROI time
352
+ series, after Glerean et al. (2012): the data are narrow-band filtered
353
+ (Butterworth) and Hilbert-transformed to get each subject's instantaneous
354
+ phase angle at every time point. Across subjects, the result gives the
355
+ mean phase angle, the mean resultant vector length, and a parametric
356
+ p-value from the Rayleigh test for circular uniformity (Fisher, 1995).
357
+ With `pairwise=True` these are computed on pairwise phase-angle differences
358
+ (inter-site phase coupling in the EEG literature) rather than on the raw
359
+ angles (inter-trial phase coupling).
360
+
361
+ The default band, 0.04-0.07 Hz, follows Glerean et al. (2012). It is close
362
+ to the "slow-4" band (0.025-0.067 Hz; Zuo et al., 2010; Penttonen &
363
+ Buzsáki, 2003) but excludes ~0.03 Hz, which carries aliased respiration
364
+ (Birn et al., 2006).
365
+
366
+ Args:
367
+ data (np.ndarray | pl.DataFrame | pd.DataFrame): Observations by
368
+ subjects.
369
+ sampling_freq (float): Sampling frequency in Hz. Defaults to 0.5.
370
+ low_cut (float): Lower band-pass cutoff in Hz. Defaults to 0.04.
371
+ high_cut (float): Upper band-pass cutoff in Hz. Defaults to 0.07.
372
+ order (int): Butterworth filter order. Defaults to 5.
373
+ pairwise (bool): Compute on pairwise phase-angle differences instead of
374
+ the raw phase angles. Defaults to False.
375
+
376
+ Returns:
377
+ dict: Keys `'average_angle'` (np.ndarray, mean phase angle per time
378
+ point), `'vector_length'` (np.ndarray, mean resultant length per
379
+ time point), and `'p'` (np.ndarray, Rayleigh-test p-value per time
380
+ point).
381
+
382
+ References:
383
+ Birn, R. M., Smith, M. A., Bandettini, P. A., & Diamond, J. B. (2006).
384
+ Separating respiratory-variation-related fluctuations from
385
+ neuronal-activity-related fluctuations in fMRI. NeuroImage, 31,
386
+ 1536-1548.
387
+
388
+ Buzsáki, G., & Draguhn, A. (2004). Neuronal oscillations in cortical
389
+ networks. Science, 304(5679), 1926-1929.
390
+
391
+ Fisher, N. I. (1995). Statistical analysis of circular data. Cambridge
392
+ University Press.
393
+
394
+ Glerean, E., Salmi, J., Lahnakoski, J. M., Jääskeläinen, I. P., & Sams,
395
+ M. (2012). Functional magnetic resonance imaging phase synchronization
396
+ as a measure of dynamic functional connectivity. Brain Connectivity,
397
+ 2(2), 91-101.
398
+ """
399
+ data_array = _as_ndarray(data)
400
+ phase = np.angle(
401
+ hilbert(
402
+ _butter_bandpass_filter(
403
+ data_array, low_cut, high_cut, sampling_freq, order=order
404
+ ),
405
+ axis=0,
406
+ )
407
+ )
408
+
409
+ if pairwise:
410
+ phase = np.array(
411
+ [
412
+ phase[:, i] - phase[:, j]
413
+ for i in range(phase.shape[1])
414
+ for j in range(phase.shape[1])
415
+ if i < j
416
+ ]
417
+ ).T
418
+
419
+ out = {"average_angle": _phase_mean_angle(phase)}
420
+ out["vector_length"] = _phase_vector_length(phase)
421
+ out["p"] = _phase_rayleigh_p(phase)
422
+ return out