voxelops 0.1.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.
- voxelops/__init__.py +98 -0
- voxelops/exceptions.py +158 -0
- voxelops/runners/__init__.py +13 -0
- voxelops/runners/_base.py +191 -0
- voxelops/runners/heudiconv.py +202 -0
- voxelops/runners/qsiparc.py +150 -0
- voxelops/runners/qsiprep.py +187 -0
- voxelops/runners/qsirecon.py +173 -0
- voxelops/schemas/__init__.py +41 -0
- voxelops/schemas/heudiconv.py +121 -0
- voxelops/schemas/qsiparc.py +107 -0
- voxelops/schemas/qsiprep.py +140 -0
- voxelops/schemas/qsirecon.py +154 -0
- voxelops/utils/__init__.py +1 -0
- voxelops/utils/bids.py +486 -0
- voxelops-0.1.0.dist-info/METADATA +221 -0
- voxelops-0.1.0.dist-info/RECORD +19 -0
- voxelops-0.1.0.dist-info/WHEEL +4 -0
- voxelops-0.1.0.dist-info/licenses/LICENSE +21 -0
voxelops/utils/bids.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""BIDS post-processing utilities for HeudiConv output."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import stat
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _run_post_processing_step(
|
|
10
|
+
step_func: Callable,
|
|
11
|
+
step_name: str,
|
|
12
|
+
results: Dict[str, Any],
|
|
13
|
+
*args,
|
|
14
|
+
**kwargs,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Helper to run a post-processing step and record its results."""
|
|
17
|
+
try:
|
|
18
|
+
step_result = step_func(*args, **kwargs)
|
|
19
|
+
results[step_name] = step_result
|
|
20
|
+
|
|
21
|
+
if not step_result["success"]:
|
|
22
|
+
results["errors"].extend(step_result.get("errors", []))
|
|
23
|
+
results["success"] = False
|
|
24
|
+
except Exception as e:
|
|
25
|
+
results["errors"].append(f"{step_name.capitalize()} failed: {e}")
|
|
26
|
+
results["success"] = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def post_process_heudiconv_output(
|
|
30
|
+
bids_dir: Path,
|
|
31
|
+
participant: str,
|
|
32
|
+
session: Optional[str] = None,
|
|
33
|
+
dry_run: bool = False,
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
"""
|
|
36
|
+
Post-process HeudiConv output to ensure BIDS compliance.
|
|
37
|
+
|
|
38
|
+
Orchestrates all post-processing steps:
|
|
39
|
+
|
|
40
|
+
1. Verify fieldmap EPI files exist
|
|
41
|
+
2. Add IntendedFor fields to fmap JSONs
|
|
42
|
+
3. Hide bval/bvec from fmap directories (rename with dot prefix)
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
bids_dir : Path
|
|
47
|
+
Root BIDS directory.
|
|
48
|
+
participant : str
|
|
49
|
+
Participant ID (without 'sub-' prefix).
|
|
50
|
+
session : Optional[str], optional
|
|
51
|
+
Session ID (without 'ses-' prefix), if applicable, by default None.
|
|
52
|
+
dry_run : bool, optional
|
|
53
|
+
If True, report changes without modifying files, by default False.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
Dict[str, Any]
|
|
58
|
+
A dictionary with results:
|
|
59
|
+
|
|
60
|
+
- 'success': bool
|
|
61
|
+
- 'verification': dict
|
|
62
|
+
- 'intended_for': dict
|
|
63
|
+
- 'cleanup': dict
|
|
64
|
+
- 'errors': list
|
|
65
|
+
"""
|
|
66
|
+
results = {
|
|
67
|
+
"success": True,
|
|
68
|
+
"errors": [],
|
|
69
|
+
"verification": {},
|
|
70
|
+
"intended_for": {},
|
|
71
|
+
"cleanup": {},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Build participant directory path
|
|
75
|
+
participant_dir = bids_dir / f"sub-{participant}"
|
|
76
|
+
if session:
|
|
77
|
+
participant_dir = participant_dir / f"ses-{session}"
|
|
78
|
+
|
|
79
|
+
if not participant_dir.exists():
|
|
80
|
+
results["success"] = False
|
|
81
|
+
results["errors"].append(f"Participant directory not found: {participant_dir}")
|
|
82
|
+
return results
|
|
83
|
+
|
|
84
|
+
# Step 1: Verify fieldmap EPI files exist
|
|
85
|
+
_run_post_processing_step(
|
|
86
|
+
verify_fmap_epi_files,
|
|
87
|
+
"verification",
|
|
88
|
+
results,
|
|
89
|
+
participant_dir,
|
|
90
|
+
session,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Step 2: Add IntendedFor to fieldmap JSONs
|
|
94
|
+
_run_post_processing_step(
|
|
95
|
+
add_intended_for_to_fmaps,
|
|
96
|
+
"intended_for",
|
|
97
|
+
results,
|
|
98
|
+
participant_dir,
|
|
99
|
+
session,
|
|
100
|
+
dry_run,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Step 3: Hide bval/bvec from fmap directories
|
|
104
|
+
_run_post_processing_step(
|
|
105
|
+
remove_bval_bvec_from_fmaps,
|
|
106
|
+
"cleanup",
|
|
107
|
+
results,
|
|
108
|
+
participant_dir,
|
|
109
|
+
session,
|
|
110
|
+
dry_run,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return results
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def verify_fmap_epi_files(
|
|
117
|
+
participant_dir: Path,
|
|
118
|
+
session: Optional[str] = None,
|
|
119
|
+
) -> Dict[str, Any]:
|
|
120
|
+
"""
|
|
121
|
+
Verify that expected fieldmap EPI files exist.
|
|
122
|
+
|
|
123
|
+
Checks for existence of ``*acq-dwi*_epi.nii.gz`` and ``.json`` in ``fmap/`` directory.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
participant_dir : Path
|
|
128
|
+
Path to participant directory (or session directory if session exists).
|
|
129
|
+
session : Optional[str], optional
|
|
130
|
+
Session ID (for logging purposes), by default None.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
Dict[str, Any]
|
|
135
|
+
Dictionary with verification results.
|
|
136
|
+
"""
|
|
137
|
+
results = {
|
|
138
|
+
"success": True,
|
|
139
|
+
"found_files": [],
|
|
140
|
+
"missing_files": [],
|
|
141
|
+
"errors": [],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fmap_dir = participant_dir / "fmap"
|
|
145
|
+
|
|
146
|
+
if not fmap_dir.exists():
|
|
147
|
+
results["success"] = False
|
|
148
|
+
results["errors"].append(f"Fieldmap directory not found: {fmap_dir}")
|
|
149
|
+
return results
|
|
150
|
+
|
|
151
|
+
# Look for DWI fieldmap files
|
|
152
|
+
dwi_epi_nii = list(fmap_dir.glob("*acq-dwi*_epi.nii.gz"))
|
|
153
|
+
dwi_epi_json = list(fmap_dir.glob("*acq-dwi*_epi.json"))
|
|
154
|
+
|
|
155
|
+
if dwi_epi_nii:
|
|
156
|
+
results["found_files"].extend([str(f.name) for f in dwi_epi_nii])
|
|
157
|
+
else:
|
|
158
|
+
results["missing_files"].append("*acq-dwi*_epi.nii.gz")
|
|
159
|
+
results["errors"].append("No DWI fieldmap NIfTI files found")
|
|
160
|
+
results["success"] = False
|
|
161
|
+
|
|
162
|
+
if dwi_epi_json:
|
|
163
|
+
results["found_files"].extend([str(f.name) for f in dwi_epi_json])
|
|
164
|
+
else:
|
|
165
|
+
results["missing_files"].append("*acq-dwi*_epi.json")
|
|
166
|
+
results["errors"].append("No DWI fieldmap JSON files found")
|
|
167
|
+
results["success"] = False
|
|
168
|
+
|
|
169
|
+
return results
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _process_single_fmap_json(
|
|
173
|
+
fmap_json: Path,
|
|
174
|
+
participant_dir: Path,
|
|
175
|
+
session: Optional[str],
|
|
176
|
+
dry_run: bool,
|
|
177
|
+
results: Dict[str, Any],
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Processes a single fmap JSON file to add IntendedFor field."""
|
|
180
|
+
try:
|
|
181
|
+
# Determine acquisition type from filename
|
|
182
|
+
filename = fmap_json.name
|
|
183
|
+
|
|
184
|
+
if "acq-dwi" in filename:
|
|
185
|
+
# DWI fieldmap -> find DWI targets
|
|
186
|
+
target_files = _find_dwi_targets(participant_dir)
|
|
187
|
+
acq_type = "DWI"
|
|
188
|
+
elif "acq-func" in filename:
|
|
189
|
+
# Functional fieldmap -> find all BOLD targets
|
|
190
|
+
target_files = _find_func_targets(participant_dir)
|
|
191
|
+
acq_type = "functional"
|
|
192
|
+
else:
|
|
193
|
+
results["errors"].append(f"Unknown acquisition type in {filename}")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if not target_files:
|
|
197
|
+
results["errors"].append(f"No target files found for {filename}")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Build IntendedFor paths (relative to session or participant directory)
|
|
201
|
+
intended_for_paths = [
|
|
202
|
+
_build_intended_for_path(target, participant_dir, session)
|
|
203
|
+
for target in target_files
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
# Update JSON file
|
|
207
|
+
if not dry_run:
|
|
208
|
+
success = _update_json_sidecar(fmap_json, intended_for_paths)
|
|
209
|
+
if success:
|
|
210
|
+
results["updated_files"].append(
|
|
211
|
+
{
|
|
212
|
+
"file": str(fmap_json.name),
|
|
213
|
+
"type": acq_type,
|
|
214
|
+
"targets": intended_for_paths,
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
results["errors"].append(f"Failed to update {filename}")
|
|
219
|
+
else:
|
|
220
|
+
results["updated_files"].append(
|
|
221
|
+
{
|
|
222
|
+
"file": str(fmap_json.name),
|
|
223
|
+
"type": acq_type,
|
|
224
|
+
"targets": intended_for_paths,
|
|
225
|
+
"note": "Dry run - not modified",
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
results["errors"].append(f"Error processing {fmap_json.name}: {e}")
|
|
231
|
+
results["success"] = False
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def add_intended_for_to_fmaps(
|
|
235
|
+
participant_dir: Path,
|
|
236
|
+
session: Optional[str] = None,
|
|
237
|
+
dry_run: bool = False,
|
|
238
|
+
) -> Dict[str, Any]:
|
|
239
|
+
"""
|
|
240
|
+
Add IntendedFor fields to fieldmap JSON files.
|
|
241
|
+
|
|
242
|
+
Maps fieldmaps to target files based on acquisition type:
|
|
243
|
+
|
|
244
|
+
- ``acq-dwi*_epi.json`` -> all ``dwi/*_dwi.nii.gz`` files
|
|
245
|
+
- ``acq-func*_epi.json`` -> all ``func/*_bold.nii.gz`` files
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
participant_dir : Path
|
|
250
|
+
Path to participant directory (or session directory if session exists).
|
|
251
|
+
session : Optional[str], optional
|
|
252
|
+
Session ID (for building relative paths), by default None.
|
|
253
|
+
dry_run : bool, optional
|
|
254
|
+
If True, report changes without modifying files, by default False.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
Dict[str, Any]
|
|
259
|
+
Dictionary with processing results.
|
|
260
|
+
"""
|
|
261
|
+
results = {
|
|
262
|
+
"success": True,
|
|
263
|
+
"updated_files": [],
|
|
264
|
+
"errors": [],
|
|
265
|
+
"dry_run": dry_run,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fmap_dir = participant_dir / "fmap"
|
|
269
|
+
|
|
270
|
+
if not fmap_dir.exists():
|
|
271
|
+
results["success"] = False
|
|
272
|
+
results["errors"].append(f"Fieldmap directory not found: {fmap_dir}")
|
|
273
|
+
return results
|
|
274
|
+
|
|
275
|
+
# Find all fieldmap JSON files
|
|
276
|
+
fmap_jsons = list(fmap_dir.glob("*_epi.json"))
|
|
277
|
+
|
|
278
|
+
if not fmap_jsons:
|
|
279
|
+
results["errors"].append("No fieldmap JSON files found")
|
|
280
|
+
results["success"] = False
|
|
281
|
+
return results
|
|
282
|
+
|
|
283
|
+
for fmap_json in fmap_jsons:
|
|
284
|
+
_process_single_fmap_json(fmap_json, participant_dir, session, dry_run, results)
|
|
285
|
+
|
|
286
|
+
return results
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def remove_bval_bvec_from_fmaps(
|
|
290
|
+
participant_dir: Path,
|
|
291
|
+
session: Optional[str] = None,
|
|
292
|
+
dry_run: bool = False,
|
|
293
|
+
) -> Dict[str, Any]:
|
|
294
|
+
"""
|
|
295
|
+
Hide .bvec and .bval files from fmap directories by renaming with dot prefix.
|
|
296
|
+
|
|
297
|
+
These files are incorrectly generated by dcm2niix for fieldmaps
|
|
298
|
+
and are not BIDS-compliant for EPI fieldmaps. Instead of deleting,
|
|
299
|
+
we rename them with a leading dot to hide them (e.g., ``.filename.bvec``).
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
participant_dir : Path
|
|
304
|
+
Path to participant directory (or session directory if session exists).
|
|
305
|
+
session : Optional[str], optional
|
|
306
|
+
Session ID (for logging purposes), by default None.
|
|
307
|
+
dry_run : bool, optional
|
|
308
|
+
If True, report files to hide without renaming, by default False.
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
Dict[str, Any]
|
|
313
|
+
Dictionary with cleanup results.
|
|
314
|
+
"""
|
|
315
|
+
results = {
|
|
316
|
+
"success": True,
|
|
317
|
+
"hidden_files": [],
|
|
318
|
+
"errors": [],
|
|
319
|
+
"dry_run": dry_run,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fmap_dir = participant_dir / "fmap"
|
|
323
|
+
|
|
324
|
+
if not fmap_dir.exists():
|
|
325
|
+
results["errors"].append(f"Fieldmap directory not found: {fmap_dir}")
|
|
326
|
+
results["success"] = False
|
|
327
|
+
return results
|
|
328
|
+
|
|
329
|
+
# Find all .bvec and .bval files in fmap directory (excluding already hidden ones)
|
|
330
|
+
bvec_files = [f for f in fmap_dir.glob("*_epi.bvec") if not f.name.startswith(".")]
|
|
331
|
+
bval_files = [f for f in fmap_dir.glob("*_epi.bval") if not f.name.startswith(".")]
|
|
332
|
+
|
|
333
|
+
files_to_hide = bvec_files + bval_files
|
|
334
|
+
|
|
335
|
+
if not files_to_hide:
|
|
336
|
+
# Not an error - just means files are already clean/hidden
|
|
337
|
+
return results
|
|
338
|
+
|
|
339
|
+
for file_path in files_to_hide:
|
|
340
|
+
try:
|
|
341
|
+
if not dry_run:
|
|
342
|
+
# Rename with leading dot to hide
|
|
343
|
+
hidden_path = file_path.parent / f".{file_path.name}"
|
|
344
|
+
file_path.rename(hidden_path)
|
|
345
|
+
results["hidden_files"].append(
|
|
346
|
+
{
|
|
347
|
+
"original": str(file_path.name),
|
|
348
|
+
"hidden_as": str(hidden_path.name),
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
results["hidden_files"].append(
|
|
353
|
+
{
|
|
354
|
+
"file": str(file_path.name),
|
|
355
|
+
"will_hide_as": f".{file_path.name}",
|
|
356
|
+
"note": "Dry run - not renamed",
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
results["errors"].append(f"Failed to hide {file_path.name}: {e}")
|
|
361
|
+
results["success"] = False
|
|
362
|
+
|
|
363
|
+
return results
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# Private helper functions
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _find_dwi_targets(participant_dir: Path) -> List[Path]:
|
|
370
|
+
"""Find all DWI NIfTI files in dwi directory."""
|
|
371
|
+
dwi_dir = participant_dir / "dwi"
|
|
372
|
+
if not dwi_dir.exists():
|
|
373
|
+
return []
|
|
374
|
+
return list(dwi_dir.glob("*_dwi.nii.gz"))
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _find_func_targets(participant_dir: Path) -> List[Path]:
|
|
378
|
+
"""Find all functional BOLD NIfTI files in func directory."""
|
|
379
|
+
func_dir = participant_dir / "func"
|
|
380
|
+
if not func_dir.exists():
|
|
381
|
+
return []
|
|
382
|
+
return list(func_dir.glob("*_bold.nii.gz"))
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _build_intended_for_path(
|
|
386
|
+
target_file: Path,
|
|
387
|
+
participant_dir: Path,
|
|
388
|
+
session: Optional[str] = None,
|
|
389
|
+
) -> str:
|
|
390
|
+
"""
|
|
391
|
+
Build BIDS-compliant relative path for IntendedFor field.
|
|
392
|
+
|
|
393
|
+
Paths are relative to the session directory (if session exists)
|
|
394
|
+
or participant directory.
|
|
395
|
+
|
|
396
|
+
Parameters
|
|
397
|
+
----------
|
|
398
|
+
target_file : Path
|
|
399
|
+
Absolute path to target file.
|
|
400
|
+
participant_dir : Path
|
|
401
|
+
Path to participant/session directory.
|
|
402
|
+
session : Optional[str], optional
|
|
403
|
+
Session ID if applicable, by default None.
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
str
|
|
408
|
+
Relative path string for IntendedFor field.
|
|
409
|
+
"""
|
|
410
|
+
# Get path relative to participant_dir
|
|
411
|
+
try:
|
|
412
|
+
rel_path = target_file.relative_to(participant_dir)
|
|
413
|
+
if session: # Add "ses-{session}" before
|
|
414
|
+
rel_path = f"ses-{session}/{rel_path}"
|
|
415
|
+
return str(rel_path)
|
|
416
|
+
except ValueError:
|
|
417
|
+
# If relative_to fails, build manually
|
|
418
|
+
# This shouldn't happen if paths are constructed correctly
|
|
419
|
+
return str(target_file.name)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _update_json_sidecar(json_path: Path, intended_for: List[str]) -> bool:
|
|
423
|
+
"""
|
|
424
|
+
Update JSON sidecar file with IntendedFor field.
|
|
425
|
+
|
|
426
|
+
Reads existing JSON, adds/updates IntendedFor field, and writes back.
|
|
427
|
+
Preserves all existing fields. Handles read-only files by making them writable.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
json_path : Path
|
|
432
|
+
Path to JSON file.
|
|
433
|
+
intended_for : List[str]
|
|
434
|
+
List of relative paths for IntendedFor field.
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
bool
|
|
439
|
+
True if successful, False otherwise.
|
|
440
|
+
"""
|
|
441
|
+
try:
|
|
442
|
+
# Read existing JSON
|
|
443
|
+
data = _read_json_sidecar(json_path)
|
|
444
|
+
if data is None:
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
# Add IntendedFor field (BIDS spec requires array)
|
|
448
|
+
data["IntendedFor"] = intended_for
|
|
449
|
+
|
|
450
|
+
# Make file writable if it's read-only (HeudiConv creates read-only files)
|
|
451
|
+
current_mode = json_path.stat().st_mode
|
|
452
|
+
if not (current_mode & stat.S_IWUSR):
|
|
453
|
+
# Add user write permission
|
|
454
|
+
json_path.chmod(current_mode | stat.S_IWUSR)
|
|
455
|
+
|
|
456
|
+
# Write back with formatting
|
|
457
|
+
with open(json_path, "w") as f:
|
|
458
|
+
json.dump(data, f, indent=2)
|
|
459
|
+
|
|
460
|
+
return True
|
|
461
|
+
|
|
462
|
+
except Exception as e:
|
|
463
|
+
print(f"Error updating {json_path}: {e}")
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _read_json_sidecar(json_path: Path) -> Optional[Dict[str, Any]]:
|
|
468
|
+
"""
|
|
469
|
+
Read JSON sidecar file with error handling.
|
|
470
|
+
|
|
471
|
+
Parameters
|
|
472
|
+
----------
|
|
473
|
+
json_path : Path
|
|
474
|
+
Path to JSON file.
|
|
475
|
+
|
|
476
|
+
Returns
|
|
477
|
+
-------
|
|
478
|
+
Optional[Dict[str, Any]]
|
|
479
|
+
Dictionary with JSON contents, or None if reading fails.
|
|
480
|
+
"""
|
|
481
|
+
try:
|
|
482
|
+
with open(json_path) as f:
|
|
483
|
+
return json.load(f)
|
|
484
|
+
except Exception as e:
|
|
485
|
+
print(f"Error reading {json_path}: {e}")
|
|
486
|
+
return None
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voxelops
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Clean, simple neuroimaging pipeline automation for brain banks
|
|
5
|
+
Project-URL: Homepage, https://github.com/yalab-devops/VoxelOps
|
|
6
|
+
Project-URL: Documentation, https://github.com/yalab-devops/VoxelOps#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/yalab-devops/VoxelOps
|
|
8
|
+
Project-URL: Issues, https://github.com/yalab-devops/VoxelOps/issues
|
|
9
|
+
Author-email: YALab DevOps <yalab.dev@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: brain-bank,docker,heudiconv,neuroimaging,qsiprep,qsirecon
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: ipython>=8.12.3
|
|
24
|
+
Requires-Dist: pandas>=2.0.3
|
|
25
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
26
|
+
Requires-Dist: templateflow>=24.2.2
|
|
27
|
+
Provides-Extra: config
|
|
28
|
+
Requires-Dist: pyyaml>=6.0; extra == 'config'
|
|
29
|
+
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'config'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: black>=23.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pre-commit>=3.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
|
|
38
|
+
Requires-Dist: sphinx>=7.0; extra == 'docs'
|
|
39
|
+
Provides-Extra: notebooks
|
|
40
|
+
Requires-Dist: jupyter>=1.0; extra == 'notebooks'
|
|
41
|
+
Requires-Dist: matplotlib>=3.5; extra == 'notebooks'
|
|
42
|
+
Requires-Dist: pandas>=1.3; extra == 'notebooks'
|
|
43
|
+
Requires-Dist: seaborn>=0.11; extra == 'notebooks'
|
|
44
|
+
Description-Content-Type: text/x-rst
|
|
45
|
+
|
|
46
|
+
VoxelOps
|
|
47
|
+
========
|
|
48
|
+
|
|
49
|
+
.. image:: https://github.com/GalKepler/VoxelOps/blob/main/docs/images/Gemini_Generated_Image_m9bi47m9bi47m9bi.png?raw=true
|
|
50
|
+
:alt: VoxelOps Logo
|
|
51
|
+
|
|
52
|
+
Clean, simple neuroimaging pipeline automation for brain banks.
|
|
53
|
+
---------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
Brain banks need to process neuroimaging data **consistently**, **reproducibly**, and **auditably**. VoxelOps makes that simple by wrapping Docker-based neuroimaging tools into clean Python functions that return plain dicts -- ready for your database, your logs, and your peace of mind.
|
|
56
|
+
|
|
57
|
+
========
|
|
58
|
+
Overview
|
|
59
|
+
========
|
|
60
|
+
|
|
61
|
+
.. list-table::
|
|
62
|
+
:stub-columns: 1
|
|
63
|
+
|
|
64
|
+
* - docs
|
|
65
|
+
- |docs|
|
|
66
|
+
* - tests, CI & coverage
|
|
67
|
+
- |github-actions| |codecov| |codacy|
|
|
68
|
+
* - version
|
|
69
|
+
- |pypi| |python|
|
|
70
|
+
* - styling
|
|
71
|
+
- |black| |isort| |flake8| |pre-commit|
|
|
72
|
+
* - license
|
|
73
|
+
- |license|
|
|
74
|
+
|
|
75
|
+
.. |docs| image:: https://readthedocs.org/projects/voxelops/badge/?version=latest
|
|
76
|
+
:target: https://voxelops.readthedocs.io/en/latest/?badge=latest
|
|
77
|
+
:alt: Documentation Status
|
|
78
|
+
|
|
79
|
+
.. |github-actions| image:: https://github.com/GalKepler/VoxelOps/actions/workflows/ci.yml/badge.svg
|
|
80
|
+
:target: https://github.com/GalKepler/VoxelOps/actions/workflows/ci.yml
|
|
81
|
+
:alt: CI
|
|
82
|
+
|
|
83
|
+
.. |codecov| image:: https://codecov.io/gh/GalKepler/VoxelOps/graph/badge.svg?token=GBOLQOB5VI
|
|
84
|
+
:target: https://codecov.io/gh/GalKepler/VoxelOps
|
|
85
|
+
:alt: codecov
|
|
86
|
+
|
|
87
|
+
.. |codacy| image:: https://app.codacy.com/project/badge/Grade/84bfb76385244fc3b80bc18e5c8f3bfd
|
|
88
|
+
:target: https://app.codacy.com/gh/GalKepler/VoxelOps/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade
|
|
89
|
+
:alt: Codacy Badge
|
|
90
|
+
|
|
91
|
+
.. |pypi| image:: https://badge.fury.io/py/voxelops.svg
|
|
92
|
+
:target: https://badge.fury.io/py/voxelops
|
|
93
|
+
:alt: PyPI version
|
|
94
|
+
|
|
95
|
+
.. |python| image:: https://img.shields.io/badge/python-3.10%2B-blue.svg
|
|
96
|
+
:target: https://www.python.org/downloads/
|
|
97
|
+
:alt: Python 3.10+
|
|
98
|
+
|
|
99
|
+
.. |license| image:: https://img.shields.io/github/license/yalab-devops/yalab-procedures.svg
|
|
100
|
+
:target: https://opensource.org/license/mit
|
|
101
|
+
:alt: License
|
|
102
|
+
|
|
103
|
+
.. |black| image:: https://img.shields.io/badge/formatter-black-000000.svg
|
|
104
|
+
:target: https://github.com/psf/black
|
|
105
|
+
|
|
106
|
+
.. |isort| image:: https://img.shields.io/badge/imports-isort-%231674b1.svg
|
|
107
|
+
:target: https://pycqa.github.io/isort/
|
|
108
|
+
|
|
109
|
+
.. |flake8| image:: https://img.shields.io/badge/style-flake8-000000.svg
|
|
110
|
+
:target: https://flake8.pycqa.org/en/latest/
|
|
111
|
+
|
|
112
|
+
.. |pre-commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white
|
|
113
|
+
:target: https://github.com/pre-commit/pre-commit
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
Features
|
|
119
|
+
--------
|
|
120
|
+
|
|
121
|
+
- **Simple Functions** -- No classes, no inheritance -- just ``run_*()`` functions that return dicts
|
|
122
|
+
- **Clear Schemas** -- Typed dataclass inputs, outputs, and defaults for every procedure
|
|
123
|
+
- **Reproducibility** -- The exact Docker command is stored in every execution record
|
|
124
|
+
- **Database-Ready** -- Results are plain dicts, trivial to save to PostgreSQL, MongoDB, or JSON
|
|
125
|
+
- **Brain Bank Defaults** -- Define your standard parameters once, reuse across all participants
|
|
126
|
+
- **Comprehensive Logging** -- Every run logged to JSON with timestamps, duration, and exit codes
|
|
127
|
+
|
|
128
|
+
Installation
|
|
129
|
+
------------
|
|
130
|
+
|
|
131
|
+
.. code-block:: bash
|
|
132
|
+
|
|
133
|
+
pip install voxelops
|
|
134
|
+
|
|
135
|
+
For development:
|
|
136
|
+
|
|
137
|
+
.. code-block:: bash
|
|
138
|
+
|
|
139
|
+
git clone https://github.com/yalab-devops/VoxelOps.git
|
|
140
|
+
cd VoxelOps
|
|
141
|
+
pip install -e ".[dev]"
|
|
142
|
+
|
|
143
|
+
**Requirements**: Python >= 3.8, Docker installed and accessible.
|
|
144
|
+
|
|
145
|
+
Quick Start
|
|
146
|
+
-----------
|
|
147
|
+
|
|
148
|
+
.. code-block:: python
|
|
149
|
+
|
|
150
|
+
from voxelops import run_qsiprep, QSIPrepInputs
|
|
151
|
+
|
|
152
|
+
inputs = QSIPrepInputs(
|
|
153
|
+
bids_dir="/data/bids",
|
|
154
|
+
participant="01",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
result = run_qsiprep(inputs, nprocs=16)
|
|
158
|
+
|
|
159
|
+
print(f"Completed in: {result['duration_human']}")
|
|
160
|
+
print(f"Outputs: {result['expected_outputs'].qsiprep_dir}")
|
|
161
|
+
print(f"Command: {' '.join(result['command'])}")
|
|
162
|
+
|
|
163
|
+
Available Procedures
|
|
164
|
+
--------------------
|
|
165
|
+
|
|
166
|
+
.. list-table::
|
|
167
|
+
:header-rows: 1
|
|
168
|
+
:widths: 15 35 25 25
|
|
169
|
+
|
|
170
|
+
* - Procedure
|
|
171
|
+
- Purpose
|
|
172
|
+
- Function
|
|
173
|
+
- Execution
|
|
174
|
+
* - HeudiConv
|
|
175
|
+
- DICOM to BIDS conversion
|
|
176
|
+
- ``run_heudiconv()``
|
|
177
|
+
- Docker
|
|
178
|
+
* - QSIPrep
|
|
179
|
+
- Diffusion MRI preprocessing
|
|
180
|
+
- ``run_qsiprep()``
|
|
181
|
+
- Docker
|
|
182
|
+
* - QSIRecon
|
|
183
|
+
- Diffusion reconstruction & connectivity
|
|
184
|
+
- ``run_qsirecon()``
|
|
185
|
+
- Docker
|
|
186
|
+
* - QSIParc
|
|
187
|
+
- Parcellation via ``parcellate``
|
|
188
|
+
- ``run_qsiparc()``
|
|
189
|
+
- Python (direct)
|
|
190
|
+
|
|
191
|
+
Brain Bank Standards
|
|
192
|
+
--------------------
|
|
193
|
+
|
|
194
|
+
Define your standard parameters once, use them everywhere:
|
|
195
|
+
|
|
196
|
+
.. code-block:: python
|
|
197
|
+
|
|
198
|
+
from voxelops import run_qsiprep, QSIPrepInputs, QSIPrepDefaults
|
|
199
|
+
|
|
200
|
+
BRAIN_BANK_QSIPREP = QSIPrepDefaults(
|
|
201
|
+
nprocs=16,
|
|
202
|
+
mem_mb=32000,
|
|
203
|
+
output_resolution=1.6,
|
|
204
|
+
anatomical_template=["MNI152NLin2009cAsym"],
|
|
205
|
+
docker_image="pennlinc/qsiprep:latest",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
for participant in participants:
|
|
209
|
+
inputs = QSIPrepInputs(bids_dir=bids_root, participant=participant)
|
|
210
|
+
result = run_qsiprep(inputs, config=BRAIN_BANK_QSIPREP)
|
|
211
|
+
db.save_processing_record(result)
|
|
212
|
+
|
|
213
|
+
Documentation
|
|
214
|
+
-------------
|
|
215
|
+
|
|
216
|
+
Full documentation is available at `voxelops.readthedocs.io <https://voxelops.readthedocs.io>`_.
|
|
217
|
+
|
|
218
|
+
License
|
|
219
|
+
-------
|
|
220
|
+
|
|
221
|
+
MIT License -- see the `LICENSE <LICENSE>`_ file for details.
|