brainfc 0.3.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 (51) hide show
  1. brainfc/__init__.py +20 -0
  2. brainfc/__main__.py +3 -0
  3. brainfc/_version.py +3 -0
  4. brainfc/atlases.py +89 -0
  5. brainfc/cli.py +184 -0
  6. brainfc/demo.py +77 -0
  7. brainfc/export.py +109 -0
  8. brainfc/imaging.py +376 -0
  9. brainfc/io.py +241 -0
  10. brainfc/models.py +335 -0
  11. brainfc/pipeline.py +383 -0
  12. brainfc/plotting.py +173 -0
  13. brainfc/preprocessing.py +179 -0
  14. brainfc/presets.py +192 -0
  15. brainfc/web/__init__.py +1 -0
  16. brainfc/web/app.py +586 -0
  17. brainfc/web/schemas.py +251 -0
  18. brainfc/web/static/THIRD_PARTY_NOTICES.txt +97 -0
  19. brainfc/web/static/app.css +1 -0
  20. brainfc/web/static/app.js +4287 -0
  21. brainfc/web/static/index.html +1 -0
  22. brainfc/web/static/reference/api-inventory.json +434 -0
  23. brainfc/web/static/reference/api-reference.html +980 -0
  24. brainfc/web/static/reference/cli-reference.html +132 -0
  25. brainfc/web/static/reference/datasets.html +55 -0
  26. brainfc/web/static/reference/eight-views-v02.png +0 -0
  27. brainfc/web/static/reference/formats.html +84 -0
  28. brainfc/web/static/reference/http-api.html +210 -0
  29. brainfc/web/static/reference/http-reference.html +2147 -0
  30. brainfc/web/static/reference/index.html +60 -0
  31. brainfc/web/static/reference/interface-preview.png +0 -0
  32. brainfc/web/static/reference/openapi.json +2185 -0
  33. brainfc/web/static/reference/outputs.html +219 -0
  34. brainfc/web/static/reference/presets-and-workflow.html +133 -0
  35. brainfc/web/static/reference/processing.html +89 -0
  36. brainfc/web/static/reference/python-api.html +276 -0
  37. brainfc/web/static/reference/quickstart.html +85 -0
  38. brainfc/web/static/reference/release.html +84 -0
  39. brainfc/web/static/reference/research.html +57 -0
  40. brainfc/web/static/reference/validation-v0.2.0.html +33 -0
  41. brainfc/web/static/reference/validation-v0.2.1.html +16 -0
  42. brainfc/web/static/reference/validation-v0.3.0.html +18 -0
  43. brainfc/web/static/reference/validation.html +42 -0
  44. brainfc/workflow.py +240 -0
  45. brainfc-0.3.0.dist-info/METADATA +97 -0
  46. brainfc-0.3.0.dist-info/RECORD +51 -0
  47. brainfc-0.3.0.dist-info/WHEEL +4 -0
  48. brainfc-0.3.0.dist-info/entry_points.txt +2 -0
  49. brainfc-0.3.0.dist-info/licenses/LICENSE +201 -0
  50. brainfc-0.3.0.dist-info/licenses/NOTICE +19 -0
  51. brainfc-0.3.0.dist-info/licenses/src/brainfc/web/static/THIRD_PARTY_NOTICES.txt +97 -0
brainfc/models.py ADDED
@@ -0,0 +1,335 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from pathlib import Path
5
+ import math
6
+ import numpy as np
7
+
8
+
9
+ class InputError(ValueError):
10
+ """Input data or declared spatial/temporal metadata are not compatible."""
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Config:
15
+ """Single-run extraction settings, shared by Python, CLI and HTTP.
16
+
17
+ Parameters
18
+ ----------
19
+ t_r : float or None, default None
20
+ Repetition time in seconds. Infer from sidecar/recognized header when None;
21
+ conflicting explicit or inferred values raise InputError (rtol=atol=1e-4).
22
+ high_pass, low_pass : float or None, default None
23
+ Cutoffs in Hz. None disables that cutoff. Positive, high_pass < low_pass,
24
+ and each enabled cutoff must be below 1/(2*t_r).
25
+ detrend : bool, default True
26
+ Remove linear trends through Nilearn signal.clean.
27
+ standardize : bool, default True
28
+ Apply sample z-score (ddof=1) to cleaned ROI time series.
29
+ discard : int, default 0
30
+ Remove this many leading original frames before temporal cleaning.
31
+ fd_threshold : float or None, default None
32
+ Censor frames with framewise_displacement > this threshold in mm.
33
+ A missing first FD is also censored when enabled. Requires confounds.
34
+ min_samples : int, default 20
35
+ Minimum retained frames; must be >=3. Not a statistical power criterion.
36
+ method : {'pearson', 'spearman', 'partial'}, default 'pearson'
37
+ Signed Pearson, tied-average-rank Spearman, or Ledoit-Wolf partial correlation.
38
+ data_space, atlas_space : str or None, default None
39
+ Exact named spaces. Images require a declared/inferred data space; when
40
+ an atlas is supplied the names must match. No registration is estimated.
41
+ preprocessed : bool, default False
42
+ Required caller declaration for every image, including CIFTI/GIFTI.
43
+ Does not perform or automatically verify motion correction/registration.
44
+ confound_columns : tuple of str or None, default None
45
+ Explicit nonempty columns to regress. None selects motion6 plus available
46
+ white_matter/csf. Nonsteady-state/FD censoring is considered independently.
47
+ table_header : bool, default True
48
+ Read first non-comment text row as ROI names. Ignored for NPY/NPZ/MAT.
49
+ transpose : bool, default False
50
+ Transpose table input to time x ROI; discards text column names.
51
+ variable : str or None, default None
52
+ NPZ/MAT numeric 2D variable; None requires exactly one eligible variable.
53
+
54
+ Notes
55
+ -----
56
+ Frozen dataclass: attributes cannot be reassigned. This is not raw-image
57
+ preprocessing. Filtering and censoring require further input-dependent checks
58
+ inside extract_connectome; construction alone does not validate a dataset."""
59
+
60
+ t_r: float | None = None
61
+ high_pass: float | None = None
62
+ low_pass: float | None = None
63
+ detrend: bool = True
64
+ standardize: bool = True
65
+ discard: int = 0
66
+ fd_threshold: float | None = None
67
+ min_samples: int = 20
68
+ method: str = "pearson"
69
+ data_space: str | None = None
70
+ atlas_space: str | None = None
71
+ preprocessed: bool = False
72
+ confound_columns: tuple[str, ...] | None = None
73
+ table_header: bool = True
74
+ transpose: bool = False
75
+ variable: str | None = None
76
+
77
+ def __post_init__(self):
78
+ for key in ("t_r", "high_pass", "low_pass", "fd_threshold"):
79
+ value = getattr(self, key)
80
+ if value is not None and (isinstance(value, bool) or not math.isfinite(value) or value <= 0):
81
+ raise InputError(f"{key} must be a positive finite number.")
82
+ if self.high_pass and self.low_pass and self.high_pass >= self.low_pass:
83
+ raise InputError("high_pass must be smaller than low_pass.")
84
+ for key, minimum in (("discard", 0), ("min_samples", 3)):
85
+ value = getattr(self, key)
86
+ if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
87
+ raise InputError(f"{key} must be an integer >= {minimum}.")
88
+ if self.method not in {"pearson", "spearman", "partial"}:
89
+ raise InputError("method must be pearson, spearman or partial (Ledoit-Wolf).")
90
+ for key in ("preprocessed", "detrend", "standardize", "table_header", "transpose"):
91
+ if not isinstance(getattr(self, key), bool):
92
+ raise InputError(f"{key} must be boolean.")
93
+ if self.confound_columns is not None:
94
+ if not self.confound_columns or any(
95
+ not isinstance(c, str) or not c for c in self.confound_columns
96
+ ):
97
+ raise InputError("confound_columns must be a nonempty sequence of column names.")
98
+
99
+ def to_dict(self):
100
+ """Return a new dataclass field dictionary.
101
+
102
+ Returns
103
+ -------
104
+ dict
105
+ All settings including None values. A tuple of confound columns remains
106
+ a tuple in Python; json.dumps serializes it as an array."""
107
+ return asdict(self)
108
+
109
+
110
+ @dataclass
111
+ class Connectome:
112
+ """Result of one run, normally constructed by extract_connectome.
113
+
114
+ Attributes
115
+ ----------
116
+ timeseries : numpy.ndarray, shape (T_retained, R)
117
+ Cleaned ROI signals in the same column order as rois.
118
+ connectivity : numpy.ndarray, shape (R, R)
119
+ Symmetric signed coefficients, diagonal 1. No display threshold applied.
120
+ fisher_z : numpy.ndarray, shape (R, R)
121
+ atanh(clip(connectivity, -1+1e-7, 1-1e-7)); diagonal 0.
122
+ rois : list of dict
123
+ ROI ID/name/label/hemisphere/network/abbreviation and coordinates or None.
124
+ sample_indices : numpy.ndarray, shape (T_retained,)
125
+ Zero-based frame positions in the original source, in increasing order.
126
+ qc, provenance : dict
127
+ Quality counts/warnings and resolved configuration, hashes and versions.
128
+ geometry : dict or None
129
+ Display-only mesh and spatial metadata; None without complete coordinates.
130
+
131
+ Notes
132
+ -----
133
+ Arrays/dictionaries are mutable. Direct construction does not validate their
134
+ shape or consistency; prefer extract_connectome for a validated result."""
135
+
136
+ timeseries: np.ndarray
137
+ connectivity: np.ndarray
138
+ fisher_z: np.ndarray
139
+ rois: list[dict]
140
+ sample_indices: np.ndarray
141
+ qc: dict
142
+ provenance: dict
143
+ geometry: dict | None = field(default=None, repr=False)
144
+
145
+ def to_dict(self, *, include_timeseries=False):
146
+ """Return schema-version-1 report data with arrays converted to lists.
147
+
148
+ Parameters
149
+ ----------
150
+ include_timeseries : bool, default False
151
+ Include cleaned time series, which may make the payload large.
152
+
153
+ Returns
154
+ -------
155
+ dict
156
+ ROI metadata, matrices, frame indices, QC, provenance and geometry.
157
+ Nested metadata dictionaries are shared, not deep-copied."""
158
+ result = {
159
+ "schema_version": 1,
160
+ "rois": self.rois,
161
+ "connectivity": self.connectivity.tolist(),
162
+ "fisher_z": self.fisher_z.tolist(),
163
+ "sample_indices": self.sample_indices.tolist(),
164
+ "qc": self.qc,
165
+ "provenance": self.provenance,
166
+ "geometry": self.geometry,
167
+ }
168
+ if include_timeseries:
169
+ result["timeseries"] = self.timeseries.tolist()
170
+ return result
171
+
172
+ def save(self, directory: str | Path, *, figures=True, report=True):
173
+ """Export one complete result to a new directory.
174
+
175
+ Parameters
176
+ ----------
177
+ directory : str or pathlib.Path
178
+ New destination; parents are created. Existing destinations are refused.
179
+ figures : bool, default True
180
+ Save matrix PNG/SVG/PDF and eight views when all ROIs have coordinates.
181
+ report : bool, default True
182
+ Save bundled, self-contained interactive report.html.
183
+
184
+ Returns
185
+ -------
186
+ pathlib.Path
187
+ Absolute output directory, promoted from a temporary sibling on success.
188
+
189
+ Raises
190
+ ------
191
+ FileExistsError
192
+ Destination already exists. Temporary exports are removed on failure.
193
+
194
+ Notes
195
+ -----
196
+ Always writes numeric arrays/tables, ROI/frame tables, QC/provenance/result JSON
197
+ and a SHA-256 manifest. Static views use threshold=0.3 and max_edges=200.
198
+ ZIP creation is performed by the web service, not this method."""
199
+ from .export import save_result
200
+
201
+ return save_result(self, directory, figures=figures, report=report)
202
+
203
+ def plot_matrix(self, path=None):
204
+ """Plot the complete signed connectivity matrix.
205
+
206
+ Parameters
207
+ ----------
208
+ path : str, pathlib.Path or None, default None
209
+ Optional image filename; suffix determines format. Parent must exist.
210
+
211
+ Returns
212
+ -------
213
+ matplotlib.figure.Figure
214
+ Figure, closed after saving when path is supplied. With None the caller
215
+ owns the figure and should close it after use.
216
+
217
+ Notes
218
+ -----
219
+ Uses Agg and a fixed [-1, 1] color range. Saves at 300 dpi. Unlike save/view,
220
+ plotting to an existing filename overwrites that image."""
221
+ from .plotting import plot_matrix
222
+
223
+ return plot_matrix(self, path)
224
+
225
+ def plot_views(
226
+ self, path=None, *, threshold=0.3, max_edges=200, selection=None, opacity=0.28, theme="paper"
227
+ ):
228
+ """Plot eight fixed anatomical projections using one filtered edge set.
229
+
230
+ Parameters
231
+ ----------
232
+ path : str, pathlib.Path or None, default None
233
+ Image filename (may overwrite) or return an unsaved figure.
234
+ threshold : float, default 0.3
235
+ Inclusive absolute coefficient cutoff in [0,1]; exact zero edges excluded.
236
+ max_edges : int, default 200
237
+ Nonnegative cap after filtering; 0 shows nodes without connections.
238
+ selection : dict or None, default None
239
+ {'kind':'node','id':roi_id} selects incident edges. Edge selection uses
240
+ {'kind':'edge','id':'i:j'} with zero-based row indices i < j. IDs are strings.
241
+ opacity : float, default 0.28
242
+ Display control in [0,1]; Matplotlib shell alpha is opacity*0.15.
243
+ theme : {'paper', 'midnight'}, default 'paper'
244
+ Light or dark background.
245
+
246
+ Returns
247
+ -------
248
+ matplotlib.figure.Figure
249
+ 2x4 orthographic views, closed if saved. Shell is rasterized even in SVG/PDF.
250
+
251
+ Raises
252
+ ------
253
+ InputError
254
+ Missing ROI coordinates, invalid selection or invalid display settings.
255
+
256
+ Notes
257
+ -----
258
+ Filter by threshold and selection, then stable-sort by descending absolute
259
+ strength and cap. Ties retain upper-triangle row-major order. Original matrices
260
+ are unchanged. Static materials differ from the GUI's WebGL renderer."""
261
+ from .plotting import plot_views
262
+
263
+ return plot_views(
264
+ self,
265
+ path,
266
+ threshold=threshold,
267
+ max_edges=max_edges,
268
+ selection=selection,
269
+ opacity=opacity,
270
+ theme=theme,
271
+ )
272
+
273
+ def view(self, path="connectome.html", *, open_browser=False):
274
+ """Write a self-contained interactive HTML report.
275
+
276
+ Parameters
277
+ ----------
278
+ path : str or pathlib.Path, default 'connectome.html'
279
+ New HTML file. Parent directories are created; overwrite is refused.
280
+ open_browser : bool, default False
281
+ Ask the operating system browser to open the new local file.
282
+
283
+ Returns
284
+ -------
285
+ pathlib.Path
286
+ Absolute HTML path. No running web server is needed for the export.
287
+
288
+ Raises
289
+ ------
290
+ FileExistsError
291
+ Output already exists.
292
+ InputError
293
+ Bundled frontend assets are missing.
294
+
295
+ Notes
296
+ -----
297
+ Embeds matrices and provenance including local input paths. Without coordinates
298
+ the report still shows matrix/QC. Requires browser WebGL for the 3D view."""
299
+ from .export import write_report
300
+
301
+ target = write_report(self, path)
302
+ if open_browser:
303
+ import webbrowser
304
+
305
+ webbrowser.open(target.as_uri())
306
+ return target
307
+
308
+ def to_hicbrain(self):
309
+ """Create a connectivity BrainDataset in separately installed Hyper-Brain.
310
+
311
+ Returns
312
+ -------
313
+ hicbrain.BrainDataset
314
+ A copy of the signed matrix with ROI IDs, names, optional coordinates and
315
+ brainfc provenance in metadata. No hyperedges are inferred.
316
+
317
+ Raises
318
+ ------
319
+ ImportError
320
+ The independent hicbrain import is unavailable. Hyper-Brain is optional."""
321
+ try:
322
+ from hicbrain import BrainDataset
323
+ except ImportError as exc:
324
+ raise ImportError("Install Hyper-Brain separately to use to_hicbrain().") from exc
325
+ coordinates = (
326
+ [r["coordinates"] for r in self.rois] if all(r.get("coordinates") for r in self.rois) else None
327
+ )
328
+ return BrainDataset(
329
+ self.connectivity.copy(),
330
+ "connectivity",
331
+ roi_ids=[r["roi_id"] for r in self.rois],
332
+ labels=[r["name"] for r in self.rois],
333
+ coordinates=coordinates,
334
+ metadata={"brainfc": self.provenance},
335
+ )