dfc-kit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. dfc_kit-1.0.0.dist-info/METADATA +184 -0
  2. dfc_kit-1.0.0.dist-info/RECORD +71 -0
  3. dfc_kit-1.0.0.dist-info/WHEEL +5 -0
  4. dfc_kit-1.0.0.dist-info/entry_points.txt +2 -0
  5. dfc_kit-1.0.0.dist-info/licenses/LICENSE +29 -0
  6. dfc_kit-1.0.0.dist-info/top_level.txt +1 -0
  7. dfckit/__init__.py +11 -0
  8. dfckit/_arrays.py +13 -0
  9. dfckit/_preprocessing.py +46 -0
  10. dfckit/_validation.py +87 -0
  11. dfckit/artifacts/__init__.py +36 -0
  12. dfckit/artifacts/_fields.py +67 -0
  13. dfckit/artifacts/_json.py +92 -0
  14. dfckit/artifacts/_numpy.py +55 -0
  15. dfckit/artifacts/models.py +624 -0
  16. dfckit/artifacts/state_alignment.py +106 -0
  17. dfckit/artifacts/state_results.py +353 -0
  18. dfckit/artifacts/state_scoring.py +509 -0
  19. dfckit/artifacts/state_stability.py +207 -0
  20. dfckit/cli.py +105 -0
  21. dfckit/commands/__init__.py +1 -0
  22. dfckit/commands/parser.py +354 -0
  23. dfckit/commands/reporting.py +146 -0
  24. dfckit/commands/source.py +245 -0
  25. dfckit/commands/stability.py +288 -0
  26. dfckit/commands/states.py +266 -0
  27. dfckit/connectivity/__init__.py +68 -0
  28. dfckit/connectivity/_edge_products.py +130 -0
  29. dfckit/connectivity/correlation.py +61 -0
  30. dfckit/connectivity/instantaneous.py +253 -0
  31. dfckit/connectivity/leida.py +204 -0
  32. dfckit/connectivity/lowrank.py +384 -0
  33. dfckit/connectivity/partition.py +291 -0
  34. dfckit/connectivity/windows.py +83 -0
  35. dfckit/data.py +229 -0
  36. dfckit/inference/__init__.py +55 -0
  37. dfckit/inference/endpoints.py +194 -0
  38. dfckit/inference/hc3.py +207 -0
  39. dfckit/inference/matching.py +300 -0
  40. dfckit/inference/multiple_testing.py +52 -0
  41. dfckit/inference/nbs.py +586 -0
  42. dfckit/inference/paired.py +157 -0
  43. dfckit/inference/state_metrics.py +277 -0
  44. dfckit/information/__init__.py +45 -0
  45. dfckit/information/_artifact.py +440 -0
  46. dfckit/information/estimators.py +536 -0
  47. dfckit/information/fixed.py +660 -0
  48. dfckit/information/summary.py +89 -0
  49. dfckit/io/__init__.py +23 -0
  50. dfckit/io/xcpd.py +470 -0
  51. dfckit/reference.py +350 -0
  52. dfckit/segments.py +44 -0
  53. dfckit/states/__init__.py +97 -0
  54. dfckit/states/alignment.py +364 -0
  55. dfckit/states/cap.py +62 -0
  56. dfckit/states/cross_validation.py +114 -0
  57. dfckit/states/data.py +387 -0
  58. dfckit/states/hmm.py +387 -0
  59. dfckit/states/interpretation.py +273 -0
  60. dfckit/states/kmeans.py +340 -0
  61. dfckit/states/metrics.py +94 -0
  62. dfckit/states/scoring.py +114 -0
  63. dfckit/states/selection.py +516 -0
  64. dfckit/states/stability.py +183 -0
  65. dfckit/states/streaming.py +859 -0
  66. dfckit/states/streaming_hmm.py +436 -0
  67. dfckit/storage/__init__.py +29 -0
  68. dfckit/storage/_statistics.py +80 -0
  69. dfckit/storage/builders.py +330 -0
  70. dfckit/storage/store.py +672 -0
  71. dfckit/storage/summary.py +140 -0
@@ -0,0 +1,253 @@
1
+ """Instantaneous edge products from ETS and MTD samples.
2
+
3
+ ETS and MTD differ only in how they construct their time-aligned ROI rows.
4
+ After that step both methods produce the same edge result, summaries, state
5
+ sequences, and chunked stores. The row representation is deliberately
6
+ private: callers normally need the edge result, not another intermediate
7
+ object to carry through the pipeline.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Sequence
13
+ from dataclasses import dataclass
14
+
15
+ import numpy as np
16
+ from numpy.typing import NDArray
17
+
18
+ from .._arrays import readonly_copy as _readonly
19
+ from .._preprocessing import _segment_standardized_samples
20
+ from ..data import TimeSeriesRun
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class _InstantaneousRows:
25
+ values: NDArray[np.float64]
26
+ starts: NDArray[np.int64]
27
+ ends: NDArray[np.int64]
28
+ segments: NDArray[np.int64]
29
+ roi_names: tuple[str, ...]
30
+ subject: str | None
31
+ session: str | None
32
+ sample_kind: str
33
+ contract: str
34
+ tr: float | None
35
+ acquisition_id: str | None
36
+
37
+
38
+ class _InstantaneousEstimator:
39
+ """Small shared implementation for the two public estimators."""
40
+
41
+ def _rows(self, run: TimeSeriesRun) -> _InstantaneousRows:
42
+ raise NotImplementedError
43
+
44
+ def transform(self, run: TimeSeriesRun) -> InstantaneousEdgeResult:
45
+ return _result_from_rows(self._rows(run), materialize_edges=True)
46
+
47
+ def rss(self, run: TimeSeriesRun) -> InstantaneousEdgeResult:
48
+ return _result_from_rows(self._rows(run), materialize_edges=False)
49
+
50
+
51
+ class ETS(_InstantaneousEstimator):
52
+ """Instantaneous edge products from segment-standardized BOLD values."""
53
+
54
+ def _rows(self, run: TimeSeriesRun) -> _InstantaneousRows:
55
+ values, indices, segments = _segment_standardized_samples(run, method_name="ETS")
56
+ return _InstantaneousRows(
57
+ values=values,
58
+ starts=indices,
59
+ ends=indices,
60
+ segments=segments,
61
+ roi_names=run.roi_names,
62
+ subject=run.subject,
63
+ session=run.session,
64
+ sample_kind="frame",
65
+ contract="ets:normalization=segment",
66
+ tr=run.tr,
67
+ acquisition_id=run.acquisition_id,
68
+ )
69
+
70
+
71
+ def _standardized_derivatives(
72
+ run: TimeSeriesRun,
73
+ ) -> tuple[NDArray[np.float64], NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]:
74
+ """Return run-standardized derivatives without crossing censor gaps."""
75
+ values: list[NDArray[np.float64]] = []
76
+ starts: list[NDArray[np.int64]] = []
77
+ ends: list[NDArray[np.int64]] = []
78
+ segments: list[NDArray[np.int64]] = []
79
+ for segment_id, positions in enumerate(run.segments()):
80
+ if len(positions) < 2:
81
+ continue
82
+ values.append(np.diff(run.values[positions], axis=0))
83
+ original = run.original_indices[positions]
84
+ starts.append(original[:-1])
85
+ ends.append(original[1:])
86
+ segments.append(np.full(len(positions) - 1, segment_id, dtype=np.int64))
87
+ if not values:
88
+ raise ValueError("MTD requires at least one valid within-segment derivative")
89
+ derivatives = np.concatenate(values, axis=0)
90
+ scale = derivatives.std(axis=0, ddof=0)
91
+ invalid = np.flatnonzero((~np.isfinite(scale)) | (scale < 1e-8))
92
+ if len(invalid):
93
+ raise ValueError(f"MTD derivative scale is undefined for ROI indices {invalid.tolist()}")
94
+ standardized = (derivatives - derivatives.mean(axis=0)) / scale
95
+ if not np.isfinite(standardized).all():
96
+ raise ValueError("standardized MTD derivatives contain non-finite values")
97
+ return standardized, np.concatenate(starts), np.concatenate(ends), np.concatenate(segments)
98
+
99
+
100
+ class MTD(_InstantaneousEstimator):
101
+ """Instantaneous edge products from standardized within-segment derivatives."""
102
+
103
+ def _rows(self, run: TimeSeriesRun) -> _InstantaneousRows:
104
+ values, starts, ends, segments = _standardized_derivatives(run)
105
+ return _InstantaneousRows(
106
+ values=values,
107
+ starts=starts,
108
+ ends=ends,
109
+ segments=segments,
110
+ roi_names=run.roi_names,
111
+ subject=run.subject,
112
+ session=run.session,
113
+ sample_kind="interval",
114
+ contract="mtd:difference=within-segment;normalization=run",
115
+ tr=run.tr,
116
+ acquisition_id=run.acquisition_id,
117
+ )
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class InstantaneousEdgeResult:
122
+ """One frame- or interval-aligned matrix of instantaneous edge products."""
123
+
124
+ features: NDArray[np.float64] | None
125
+ rss: NDArray[np.float64]
126
+ sample_start_frames: NDArray[np.int64]
127
+ sample_end_frames: NDArray[np.int64]
128
+ segment_ids: NDArray[np.int64]
129
+ edge_i: NDArray[np.int64]
130
+ edge_j: NDArray[np.int64]
131
+ roi_names: tuple[str, ...]
132
+ subject: str | None
133
+ session: str | None
134
+ sample_kind: str
135
+ source_contract: str
136
+ tr: float | None = None
137
+ acquisition_id: str | None = None
138
+
139
+ def __post_init__(self) -> None:
140
+ from .correlation import edge_index
141
+
142
+ features = None if self.features is None else np.asarray(self.features, dtype=float)
143
+ rss = np.asarray(self.rss, dtype=float)
144
+ starts = np.asarray(self.sample_start_frames, dtype=np.int64)
145
+ ends = np.asarray(self.sample_end_frames, dtype=np.int64)
146
+ segments = np.asarray(self.segment_ids, dtype=np.int64)
147
+ left = np.asarray(self.edge_i, dtype=np.int64)
148
+ right = np.asarray(self.edge_j, dtype=np.int64)
149
+ names = tuple(str(name) for name in self.roi_names)
150
+ if len(names) < 2 or len(set(names)) != len(names) or any(not name.strip() for name in names):
151
+ raise ValueError("roi_names must contain at least two unique non-empty names")
152
+ if rss.ndim != 1 or not len(rss) or not np.isfinite(rss).all():
153
+ raise ValueError("rss must be a non-empty finite one-dimensional array")
154
+ if any(array.ndim != 1 or len(array) != len(rss) for array in (starts, ends, segments)):
155
+ raise ValueError("sample metadata must align with rss")
156
+ if np.any(ends < starts) or np.any(segments < 0):
157
+ raise ValueError("sample intervals and segment IDs must be non-negative")
158
+ if len(starts) > 1 and np.any(np.diff(starts) <= 0):
159
+ raise ValueError("sample start frames must be strictly increasing")
160
+ expected_left, expected_right = edge_index(len(names))
161
+ if not np.array_equal(left, expected_left) or not np.array_equal(right, expected_right):
162
+ raise ValueError("edge endpoints must be the unique upper-triangular ROI order")
163
+ if features is not None:
164
+ if features.ndim != 2 or features.shape != (len(rss), len(left)):
165
+ raise ValueError("features must align with samples and edge endpoints")
166
+ if not np.isfinite(features).all():
167
+ raise ValueError("edge features contain non-finite samples")
168
+ if self.sample_kind not in {"frame", "interval"}:
169
+ raise ValueError("sample_kind must be 'frame' or 'interval'")
170
+ if self.sample_kind == "frame" and np.any(starts != ends):
171
+ raise ValueError("frame samples must have equal start and end frames")
172
+ if not str(self.source_contract).strip():
173
+ raise ValueError("source_contract must be non-empty")
174
+ if self.subject is not None and not str(self.subject).strip():
175
+ raise ValueError("subject cannot be empty")
176
+ if self.session is not None and not str(self.session).strip():
177
+ raise ValueError("session cannot be empty")
178
+ if self.tr is not None and (not np.isfinite(self.tr) or self.tr <= 0):
179
+ raise ValueError("tr must be finite and positive")
180
+ if self.acquisition_id is not None and not str(self.acquisition_id).strip():
181
+ raise ValueError("acquisition_id cannot be empty")
182
+ object.__setattr__(self, "features", None if features is None else _readonly(features))
183
+ object.__setattr__(self, "rss", _readonly(rss))
184
+ object.__setattr__(self, "sample_start_frames", _readonly(starts))
185
+ object.__setattr__(self, "sample_end_frames", _readonly(ends))
186
+ object.__setattr__(self, "segment_ids", _readonly(segments))
187
+ object.__setattr__(self, "edge_i", _readonly(left))
188
+ object.__setattr__(self, "edge_j", _readonly(right))
189
+ object.__setattr__(self, "roi_names", names)
190
+ object.__setattr__(self, "subject", None if self.subject is None else str(self.subject))
191
+ object.__setattr__(self, "session", None if self.session is None else str(self.session))
192
+ object.__setattr__(self, "source_contract", str(self.source_contract))
193
+ object.__setattr__(self, "acquisition_id", None if self.acquisition_id is None else str(self.acquisition_id))
194
+
195
+ @property
196
+ def n_samples(self) -> int:
197
+ return len(self.rss)
198
+
199
+ @property
200
+ def n_edges(self) -> int:
201
+ return len(self.edge_i)
202
+
203
+ @property
204
+ def n_rois(self) -> int:
205
+ return len(self.roi_names)
206
+
207
+ @property
208
+ def feature_keys(self) -> tuple[tuple[str, str], ...]:
209
+ return tuple(
210
+ (self.roi_names[int(left)], self.roi_names[int(right)])
211
+ for left, right in zip(self.edge_i, self.edge_j, strict=True)
212
+ )
213
+
214
+ def require_features(self) -> NDArray[np.float64]:
215
+ if self.features is None:
216
+ raise ValueError("this result contains RSS only and no materialized edge features")
217
+ return self.features
218
+
219
+ def cross_block(self, left: Sequence[int], right: Sequence[int]) -> NDArray[np.float64]:
220
+ from ._edge_products import cross_block_edge_summary
221
+
222
+ return cross_block_edge_summary(self, left, right)
223
+
224
+ def within_block(self, nodes: Sequence[int]) -> NDArray[np.float64]:
225
+ from ._edge_products import within_block_edge_summary
226
+
227
+ return within_block_edge_summary(self, nodes)
228
+
229
+
230
+ def _result_from_rows(rows: _InstantaneousRows, *, materialize_edges: bool) -> InstantaneousEdgeResult:
231
+ from ._edge_products import edge_products, edge_rss
232
+ from .correlation import edge_index
233
+
234
+ left, right = edge_index(len(rows.roi_names))
235
+ return InstantaneousEdgeResult(
236
+ features=edge_products(rows.values) if materialize_edges else None,
237
+ rss=edge_rss(rows.values),
238
+ sample_start_frames=rows.starts,
239
+ sample_end_frames=rows.ends,
240
+ segment_ids=rows.segments,
241
+ edge_i=left,
242
+ edge_j=right,
243
+ roi_names=rows.roi_names,
244
+ subject=rows.subject,
245
+ session=rows.session,
246
+ sample_kind=rows.sample_kind,
247
+ source_contract=rows.contract,
248
+ tr=rows.tr,
249
+ acquisition_id=rows.acquisition_id,
250
+ )
251
+
252
+
253
+ __all__ = ["ETS", "MTD", "InstantaneousEdgeResult"]
@@ -0,0 +1,204 @@
1
+ """Censor-aware Leading Eigenvector Dynamics Analysis (LEiDA)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass
7
+
8
+ import numpy as np
9
+ from numpy.typing import ArrayLike, NDArray
10
+
11
+ from .._arrays import readonly_copy as _readonly
12
+ from .._validation import validated_roi_indices
13
+ from ..data import TimeSeriesRun
14
+
15
+
16
+ def _validated_phase(values: ArrayLike) -> NDArray[np.float64]:
17
+ phase = np.asarray(values, dtype=float)
18
+ if phase.ndim != 2 or not len(phase) or phase.shape[1] < 2:
19
+ raise ValueError("phase must be a non-empty frames-by-ROI array with at least two ROIs")
20
+ if not np.isfinite(phase).all():
21
+ raise ValueError("phase contains non-finite values")
22
+ return phase
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class LEiDAResult:
27
+ """Instantaneous phase and leading phase-coherence geometry for one run."""
28
+
29
+ phase: NDArray[np.float64]
30
+ leading_vectors: NDArray[np.float64]
31
+ leading_eigenvalues: NDArray[np.float64]
32
+ original_indices: NDArray[np.int64]
33
+ segment_ids: NDArray[np.int64]
34
+ roi_names: tuple[str, ...]
35
+ subject: str | None
36
+ session: str | None
37
+ tr: float | None
38
+ minimum_segment_length: int
39
+ orientation: str
40
+ implementation: str
41
+ acquisition_id: str | None = None
42
+
43
+
44
+ def analytic_phase(values: ArrayLike) -> NDArray[np.float64]:
45
+ """Return Hilbert phase after centering each ROI over one segment."""
46
+ data = np.asarray(values, dtype=float)
47
+ if data.ndim != 2 or data.shape[0] < 3 or data.shape[1] < 2:
48
+ raise ValueError("values must have at least three frames and two ROIs")
49
+ if not np.isfinite(data).all():
50
+ raise ValueError("values contain non-finite samples")
51
+ scale = data.std(axis=0, ddof=0)
52
+ invalid = np.flatnonzero((~np.isfinite(scale)) | (scale <= 1e-12))
53
+ if len(invalid):
54
+ raise ValueError(f"Hilbert phase is undefined for constant ROI indices {invalid.tolist()}")
55
+ try:
56
+ from scipy.signal import hilbert
57
+ except ModuleNotFoundError as error:
58
+ raise ModuleNotFoundError(
59
+ "LEiDA phase extraction requires the 'phase' extra: "
60
+ "pip install 'dfc-kit[phase]'"
61
+ ) from error
62
+ centered = data - data.mean(axis=0)
63
+ return _readonly(np.angle(hilbert(centered, axis=0)))
64
+
65
+
66
+ def leading_phase_eigenvectors(
67
+ phase: ArrayLike,
68
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
69
+ """Return oriented leading eigenvectors of instantaneous phase coherence.
70
+
71
+ The matrix with entries ``cos(phase_i - phase_j)`` has rank at most two.
72
+ Solving its equivalent two-dimensional eigensystem avoids materializing a
73
+ frames-by-ROI-by-ROI tensor.
74
+ """
75
+ angles = _validated_phase(phase)
76
+ cosine = np.cos(angles)
77
+ sine = np.sin(angles)
78
+ cosine_energy = np.sum(cosine**2, axis=1)
79
+ sine_energy = np.sum(sine**2, axis=1)
80
+ cross = np.sum(cosine * sine, axis=1)
81
+ direction = 0.5 * np.arctan2(
82
+ 2.0 * cross,
83
+ cosine_energy - sine_energy,
84
+ )
85
+ vectors = (
86
+ cosine * np.cos(direction)[:, None]
87
+ + sine * np.sin(direction)[:, None]
88
+ )
89
+ norm = np.linalg.norm(vectors, axis=1)
90
+ if np.any(~np.isfinite(norm)) or np.any(norm <= 1e-12):
91
+ raise ValueError("leading phase eigenvector has undefined norm")
92
+ vectors /= norm[:, None]
93
+ vectors *= np.where(vectors.sum(axis=1, keepdims=True) < 0.0, -1.0, 1.0)
94
+
95
+ discriminant = np.sqrt(
96
+ (cosine_energy - sine_energy) ** 2 + 4.0 * cross**2
97
+ )
98
+ eigenvalues = (cosine_energy + sine_energy + discriminant) / 2.0
99
+ return _readonly(vectors), _readonly(eigenvalues)
100
+
101
+
102
+ def cross_block_phase_coherence(
103
+ phase: ArrayLike,
104
+ left: Iterable[int],
105
+ right: Iterable[int],
106
+ ) -> NDArray[np.float64]:
107
+ """Return mean instantaneous phase coherence between two disjoint blocks."""
108
+ angles = _validated_phase(phase)
109
+ left_nodes = validated_roi_indices(
110
+ left,
111
+ n_rois=angles.shape[1],
112
+ label="left block",
113
+ minimum=1,
114
+ )
115
+ right_nodes = validated_roi_indices(
116
+ right,
117
+ n_rois=angles.shape[1],
118
+ label="right block",
119
+ minimum=1,
120
+ )
121
+ overlap = sorted(set(left_nodes.tolist()).intersection(right_nodes.tolist()))
122
+ if overlap:
123
+ raise ValueError(f"cross-block phase coherence requires disjoint blocks; overlap={overlap}")
124
+ values = np.cos(
125
+ angles[:, left_nodes, None] - angles[:, None, right_nodes]
126
+ ).mean(axis=(1, 2))
127
+ return _readonly(values)
128
+
129
+
130
+ def within_block_phase_coherence(
131
+ phase: ArrayLike,
132
+ nodes: Iterable[int],
133
+ ) -> NDArray[np.float64]:
134
+ """Return off-diagonal mean instantaneous phase coherence within a block."""
135
+ angles = _validated_phase(phase)
136
+ selected = validated_roi_indices(
137
+ nodes,
138
+ n_rois=angles.shape[1],
139
+ label="within block",
140
+ minimum=2,
141
+ )
142
+ left, right = np.triu_indices(len(selected), k=1)
143
+ values = np.cos(
144
+ angles[:, selected[left]] - angles[:, selected[right]]
145
+ ).mean(axis=1)
146
+ return _readonly(values)
147
+
148
+
149
+ class LEiDA:
150
+ """Extract leading phase-coherence vectors inside retained segments."""
151
+
152
+ def __init__(self, minimum_segment_length: int = 20) -> None:
153
+ if isinstance(minimum_segment_length, (bool, np.bool_)) or not isinstance(
154
+ minimum_segment_length, (int, np.integer)
155
+ ):
156
+ raise TypeError("minimum_segment_length must be an integer")
157
+ if minimum_segment_length < 3:
158
+ raise ValueError("minimum_segment_length must be at least three")
159
+ self.minimum_segment_length = int(minimum_segment_length)
160
+
161
+ def transform(self, run: TimeSeriesRun) -> LEiDAResult:
162
+ phases: list[NDArray[np.float64]] = []
163
+ vectors: list[NDArray[np.float64]] = []
164
+ eigenvalues: list[NDArray[np.float64]] = []
165
+ original_indices: list[NDArray[np.int64]] = []
166
+ segment_ids: list[NDArray[np.int64]] = []
167
+
168
+ for segment_id, positions in enumerate(run.segments()):
169
+ if len(positions) < self.minimum_segment_length:
170
+ continue
171
+ segment_phase = analytic_phase(run.values[positions])
172
+ segment_vectors, segment_eigenvalues = leading_phase_eigenvectors(segment_phase)
173
+ phases.append(segment_phase)
174
+ vectors.append(segment_vectors)
175
+ eigenvalues.append(segment_eigenvalues)
176
+ original_indices.append(run.original_indices[positions])
177
+ segment_ids.append(np.full(len(positions), segment_id, dtype=np.int64))
178
+
179
+ if not phases:
180
+ raise ValueError(
181
+ "no retained segment meets the requested minimum_segment_length"
182
+ )
183
+ try:
184
+ import scipy
185
+ except ModuleNotFoundError as error: # pragma: no cover - analytic_phase fails first
186
+ raise ModuleNotFoundError(
187
+ "LEiDA phase extraction requires the 'phase' extra: "
188
+ "pip install 'dfc-kit[phase]'"
189
+ ) from error
190
+ return LEiDAResult(
191
+ phase=_readonly(np.concatenate(phases, axis=0)),
192
+ leading_vectors=_readonly(np.concatenate(vectors, axis=0)),
193
+ leading_eigenvalues=_readonly(np.concatenate(eigenvalues)),
194
+ original_indices=_readonly(np.concatenate(original_indices)),
195
+ segment_ids=_readonly(np.concatenate(segment_ids)),
196
+ roi_names=run.roi_names,
197
+ subject=run.subject,
198
+ session=run.session,
199
+ acquisition_id=run.acquisition_id,
200
+ tr=run.tr,
201
+ minimum_segment_length=self.minimum_segment_length,
202
+ orientation="positive-vector-sum",
203
+ implementation=f"scipy {scipy.__version__} signal.hilbert",
204
+ )