mrsiprep 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.
- mrsiprep/cli/parser.py +294 -0
- mrsiprep/cli/run.py +48 -0
- mrsiprep/config/defaults.py +22 -0
- mrsiprep/config/settings.py +212 -0
- mrsiprep/connectivity/connectivity.py +228 -0
- mrsiprep/connectivity/edges.py +17 -0
- mrsiprep/connectivity/export.py +92 -0
- mrsiprep/connectivity/nodes.py +22 -0
- mrsiprep/data/atlas/README.md +12 -0
- mrsiprep/data/atlas/chimera-LFMIHIFIS-2/chimera-LFMIHIFIS-2.nii.gz +0 -0
- mrsiprep/data/atlas/chimera-LFMIHIFIS-2/chimera-LFMIHIFIS-2.tsv +689 -0
- mrsiprep/data/atlas/chimera-LFMIHIFIS-3/chimera-LFMIHIFIS-3.nii.gz +0 -0
- mrsiprep/data/atlas/chimera-LFMIHIFIS-3/chimera-LFMIHIFIS-3.tsv +277 -0
- mrsiprep/data/atlas/chimera-LFMIHISIFF-3/chimera-LFMIHISIFF-3.nii.gz +0 -0
- mrsiprep/data/atlas/chimera-LFMIHISIFF-3/chimera-LFMIHISIFF-3.tsv +538 -0
- mrsiprep/interfaces/ants.py +362 -0
- mrsiprep/interfaces/bids_import.py +268 -0
- mrsiprep/interfaces/bids_import_gui.py +41 -0
- mrsiprep/interfaces/chimera.py +112 -0
- mrsiprep/interfaces/freesurfer.py +96 -0
- mrsiprep/interfaces/fsl.py +185 -0
- mrsiprep/io/bids.py +308 -0
- mrsiprep/io/derivatives.py +26 -0
- mrsiprep/io/loaders.py +34 -0
- mrsiprep/io/naming.py +163 -0
- mrsiprep/io/validators.py +76 -0
- mrsiprep/mrsi/filtering.py +97 -0
- mrsiprep/mrsi/masks.py +32 -0
- mrsiprep/mrsi/pvc.py +63 -0
- mrsiprep/mrsi/quality.py +63 -0
- mrsiprep/mrsi/reference.py +31 -0
- mrsiprep/mrsi/resampling.py +92 -0
- mrsiprep/parcellation/atlas_registry.py +97 -0
- mrsiprep/parcellation/base.py +18 -0
- mrsiprep/parcellation/chimera_native.py +73 -0
- mrsiprep/parcellation/extraction.py +95 -0
- mrsiprep/parcellation/labels.py +65 -0
- mrsiprep/parcellation/metprofiles.py +97 -0
- mrsiprep/parcellation/mni_atlas.py +32 -0
- mrsiprep/parcellation/parcel_fractions.py +18 -0
- mrsiprep/parcellation/synthseg.py +112 -0
- mrsiprep/parcellation/tissue_regression.py +174 -0
- mrsiprep/registration/mrsi_to_t1.py +87 -0
- mrsiprep/registration/subject_template.py +148 -0
- mrsiprep/registration/t1_to_mni.py +82 -0
- mrsiprep/registration/transforms.py +73 -0
- mrsiprep/reports/connectivity_overview.py +44 -0
- mrsiprep/reports/coverage.py +12 -0
- mrsiprep/reports/figures.py +13 -0
- mrsiprep/reports/html.py +87 -0
- mrsiprep/reports/mrsi_preproc.py +64 -0
- mrsiprep/reports/parcel_figures.py +145 -0
- mrsiprep/reports/parcel_qc.py +111 -0
- mrsiprep/reports/parcellation_overview.py +43 -0
- mrsiprep/reports/qc_combine.py +46 -0
- mrsiprep/reports/registration_overview.py +85 -0
- mrsiprep/reports/slices.py +113 -0
- mrsiprep/reports/spectra_qc.py +11 -0
- mrsiprep/reports/tissue.py +52 -0
- mrsiprep/tissue/correction.py +14 -0
- mrsiprep/tissue/fractions.py +49 -0
- mrsiprep/tissue/fuzzy_cmeans.py +132 -0
- mrsiprep/tissue/psf.py +168 -0
- mrsiprep/tissue/synthseg_fast.py +262 -0
- mrsiprep/utils/banner.py +18 -0
- mrsiprep/utils/debug.py +309 -0
- mrsiprep/utils/images.py +116 -0
- mrsiprep/utils/logging.py +50 -0
- mrsiprep/utils/misc.py +107 -0
- mrsiprep/utils/provenance.py +142 -0
- mrsiprep/utils/subprocess_utils.py +40 -0
- mrsiprep/utils/tables.py +25 -0
- mrsiprep/workflows/anatomical.py +111 -0
- mrsiprep/workflows/base.py +10 -0
- mrsiprep/workflows/connectivity.py +23 -0
- mrsiprep/workflows/mrsi.py +128 -0
- mrsiprep/workflows/nipype_engine/__init__.py +14 -0
- mrsiprep/workflows/nipype_engine/adapters.py +52 -0
- mrsiprep/workflows/nipype_engine/build.py +75 -0
- mrsiprep/workflows/nipype_engine/nodes.py +264 -0
- mrsiprep/workflows/nipype_engine/run.py +264 -0
- mrsiprep/workflows/parcellation.py +38 -0
- mrsiprep/workflows/participant.py +625 -0
- mrsiprep/workflows/registration.py +42 -0
- mrsiprep/workflows/reports.py +9 -0
- mrsiprep/workflows/tissue.py +68 -0
- mrsiprep-0.1.0.dist-info/METADATA +135 -0
- mrsiprep-0.1.0.dist-info/RECORD +92 -0
- mrsiprep-0.1.0.dist-info/WHEEL +5 -0
- mrsiprep-0.1.0.dist-info/entry_points.txt +4 -0
- mrsiprep-0.1.0.dist-info/licenses/LICENSE +57 -0
- mrsiprep-0.1.0.dist-info/top_level.txt +1 -0
mrsiprep/cli/parser.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""CLI parser for MRSIPrep."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mrsiprep.config.defaults import QUALITY_DEFAULTS
|
|
9
|
+
from mrsiprep.config.settings import MRSIPrepConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
parser = argparse.ArgumentParser(prog="mrsiprep", description="Preprocess quantified whole-brain MRSI derivatives.")
|
|
14
|
+
parser.add_argument("bids_dir", type=Path, help="The root folder of a BIDS valid dataset (sub-XXXXX folders at the top level).")
|
|
15
|
+
parser.add_argument("output_dir", type=Path, help="The output path for MRSIPrep derivatives and reports.")
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"analysis_level",
|
|
18
|
+
choices=["participant"],
|
|
19
|
+
help="Processing stage to be run, only 'participant' in the case of MRSIPrep (see BIDS-Apps specification).",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
selection = parser.add_argument_group("Options for filtering BIDS queries")
|
|
23
|
+
selection.add_argument("--participant-label", nargs="+", default=[])
|
|
24
|
+
selection.add_argument("--session-label", nargs="+", default=[])
|
|
25
|
+
selection.add_argument("--participants", type=Path, default=None, help="TSV/CSV subject-session list.")
|
|
26
|
+
selection.add_argument(
|
|
27
|
+
"--bids-filter-file",
|
|
28
|
+
type=Path,
|
|
29
|
+
default=None,
|
|
30
|
+
help="Path to a JSON file of PyBIDS-style entity filters used to select among ambiguous input candidates, "
|
|
31
|
+
"e.g. {\"t1w\": {\"acquisition\": \"memprage\", \"run\": \"01\"}} to force a specific T1w acquisition/run "
|
|
32
|
+
"when a session has more than one. Only the \"t1w\" key is currently supported.",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
quality = parser.add_argument_group("quality thresholds")
|
|
36
|
+
quality.add_argument(
|
|
37
|
+
"--metabolites",
|
|
38
|
+
type=_parse_comma_list,
|
|
39
|
+
required=True,
|
|
40
|
+
help="Comma-separated metabolite names to process, e.g. 'CrPCr,GluGln,GPCPCh,NAANAAG,Ins'.",
|
|
41
|
+
)
|
|
42
|
+
quality.add_argument("--quality-metrics", nargs="+", default=["snr", "linewidth", "crlb"])
|
|
43
|
+
quality.add_argument("--snr-min", type=float, default=QUALITY_DEFAULTS["snr_min"])
|
|
44
|
+
quality.add_argument("--linewidth-max", type=float, default=QUALITY_DEFAULTS["linewidth_max"])
|
|
45
|
+
quality.add_argument("--crlb-max", type=float, default=QUALITY_DEFAULTS["crlb_max"])
|
|
46
|
+
|
|
47
|
+
processing = parser.add_argument_group("Options for performing only a subset of the workflow")
|
|
48
|
+
processing.add_argument(
|
|
49
|
+
"--mode",
|
|
50
|
+
"--processing-mode",
|
|
51
|
+
dest="processing_mode",
|
|
52
|
+
choices=["mni-norm", "parc-con", "midas"],
|
|
53
|
+
default="mni-norm",
|
|
54
|
+
help="Processing mode. 'midas' runs a MIDAS-faithful pipeline (Maudsley et al. 2006): fuzzy c-means "
|
|
55
|
+
"tissue segmentation, PSF-convolved tissue fractions, rigid MRSI->T1 registration, and per-parcel "
|
|
56
|
+
"Eq. 4 pure-GM/pure-WM regression instead of PETPVC.",
|
|
57
|
+
)
|
|
58
|
+
processing.add_argument(
|
|
59
|
+
"--tissue-backend",
|
|
60
|
+
choices=["synthseg-fast", "existing", "none"],
|
|
61
|
+
default="synthseg-fast",
|
|
62
|
+
help="Tissue segmentation backend for PVC. 'none' disables tissue segmentation and PVC entirely. "
|
|
63
|
+
"Ignored in --mode midas, which always uses its own fuzzy c-means segmentation.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
registration = parser.add_argument_group("Specific options for registrations")
|
|
67
|
+
registration.add_argument(
|
|
68
|
+
"--registration-backend",
|
|
69
|
+
choices=["ants", "fsl", "flirt-fnirt", "flirt_fnirt", "flirt/fnirt"],
|
|
70
|
+
default="ants",
|
|
71
|
+
help="Registration toolchain. 'ants' is the default; 'fsl'/'flirt-fnirt' uses FLIRT affine registration "
|
|
72
|
+
"(no deformable stage -- FNIRT is not implemented).",
|
|
73
|
+
)
|
|
74
|
+
registration.add_argument(
|
|
75
|
+
"--ants-mrsi-to-t1-transform",
|
|
76
|
+
default="sr",
|
|
77
|
+
help="ANTs transform preset/code for MRSI-to-T1w registration. Default matches the previous implementation: 'sr'.",
|
|
78
|
+
)
|
|
79
|
+
registration.add_argument(
|
|
80
|
+
"--ants-t1-to-mni-transform",
|
|
81
|
+
default="s",
|
|
82
|
+
help="ANTs transform preset/code for T1w-to-MNI registration. Default matches the previous implementation: 's'.",
|
|
83
|
+
)
|
|
84
|
+
registration.add_argument("--fsl-mrsi-to-t1-dof", type=int, choices=[6, 7, 9, 12], default=6)
|
|
85
|
+
registration.add_argument(
|
|
86
|
+
"--fsl-mrsi-to-t1-init",
|
|
87
|
+
choices=["flirt", "usesqform"],
|
|
88
|
+
default="flirt",
|
|
89
|
+
help="FSL MRSI-to-T1w initialization. 'usesqform' applies the NIfTI qform/sform geometry with FLIRT.",
|
|
90
|
+
)
|
|
91
|
+
registration.add_argument("--fsl-t1-to-mni-dof", type=int, choices=[6, 7, 9, 12], default=12)
|
|
92
|
+
registration.add_argument("--fsl-cost", default="mutualinfo", help="FLIRT cost function for FSL registrations.")
|
|
93
|
+
registration.add_argument("--normalization", choices=["simple", "ants-syn", "existing"], default="simple")
|
|
94
|
+
registration.add_argument("--output-spaces", nargs="+", default=["MNI152NLin2009cAsym"])
|
|
95
|
+
registration.add_argument(
|
|
96
|
+
"--output-mrsi-t1w",
|
|
97
|
+
action="store_true",
|
|
98
|
+
help="Also resample all metabolite (and CRLB/SNR/FWHM/spikemask) maps into T1w space as permanent "
|
|
99
|
+
"derivatives (mrsi-t1w/). Off by default; the registration-overview report generates its own single "
|
|
100
|
+
"reference-metabolite T1w map in the work directory regardless of this flag.",
|
|
101
|
+
)
|
|
102
|
+
registration.add_argument(
|
|
103
|
+
"--mni-resolution",
|
|
104
|
+
default="t1wres",
|
|
105
|
+
help="MNI template resolution: 'origres' (MRSI native), 't1wres' (T1w native), or '<N>mm' (e.g. '2mm').",
|
|
106
|
+
)
|
|
107
|
+
registration.add_argument("--registration-t1-target", choices=["brain-csf", "brain", "raw"], default=None)
|
|
108
|
+
registration.add_argument("--csf-pv-threshold", type=float, default=0.95)
|
|
109
|
+
registration.add_argument(
|
|
110
|
+
"--ref-met",
|
|
111
|
+
required=True,
|
|
112
|
+
help="Reference metabolite map used to build the MRSI registration target, e.g. 'CrPCr'.",
|
|
113
|
+
)
|
|
114
|
+
registration.add_argument("--t1", dest="t1_pattern", default="desc-brain_T1w")
|
|
115
|
+
|
|
116
|
+
parcellation = parser.add_argument_group("parcellation")
|
|
117
|
+
parcellation.add_argument("--parcellation-mode", choices=["synthseg", "chimera", "mni"], default=None)
|
|
118
|
+
parcellation.add_argument("--synthseg-mode", choices=["fast", "standard", "robust"], default="robust")
|
|
119
|
+
parcellation.add_argument("--chimera-scheme", default="LFMIHIFIFF")
|
|
120
|
+
parcellation.add_argument("--chimera-scale", type=_parse_scale, default=3)
|
|
121
|
+
parcellation.add_argument("--chimera-grow", type=int, default=2)
|
|
122
|
+
parcellation.add_argument("--atlas", default="chimera-LFMIHIFIS-3")
|
|
123
|
+
parcellation.add_argument("--custom-atlas", type=Path, default=None)
|
|
124
|
+
parcellation.add_argument("--custom-atlas-lut", type=Path, default=None)
|
|
125
|
+
parcellation.add_argument("--fs-subjects-dir", type=Path, default=None)
|
|
126
|
+
|
|
127
|
+
connectivity = parser.add_argument_group("connectivity")
|
|
128
|
+
connectivity.add_argument("--write-connectivity", action="store_true")
|
|
129
|
+
connectivity.add_argument("--connectivity-method", choices=["pearson", "spearman", "cosine", "euclidean_distance"], default="spearman")
|
|
130
|
+
connectivity.add_argument("--connectivity-space", choices=["MRSI", "T1w", "MNI"], default="MRSI")
|
|
131
|
+
connectivity.add_argument("--connectivity-n-perturbations", type=int, default=50, help="Number of CRLB-scaled noise perturbations per metabolite used to build the connectivity similarity matrix.")
|
|
132
|
+
connectivity.add_argument("--connectivity-sigma-scale", type=float, default=2.0, help="Scale factor applied to the CRLB-derived noise sigma when perturbing metabolite maps for connectivity.")
|
|
133
|
+
connectivity.add_argument(
|
|
134
|
+
"--connectivity-exclude-parcels",
|
|
135
|
+
default=None,
|
|
136
|
+
help="Comma-separated substrings; parcels whose name contains any of them are excluded from the connectivity matrix (e.g. 'wm-lh,cer-').",
|
|
137
|
+
)
|
|
138
|
+
connectivity.add_argument(
|
|
139
|
+
"--connectivity-max-parcel-id",
|
|
140
|
+
type=int,
|
|
141
|
+
default=None,
|
|
142
|
+
help="Exclude parcels whose label/ID is greater than or equal to this value from the connectivity matrix.",
|
|
143
|
+
)
|
|
144
|
+
connectivity.add_argument("--regional-summary", choices=["mean", "median", "weighted_mean"], default="mean")
|
|
145
|
+
|
|
146
|
+
processing_control = parser.add_argument_group("Workflow configuration")
|
|
147
|
+
processing_control.add_argument("--transform", default="", help="Legacy output transform override; prefer --output-spaces.")
|
|
148
|
+
processing_control.add_argument("--no-filter", action="store_true", help="Disable biharmonic spike filtering (enabled by default in every processing mode).")
|
|
149
|
+
processing_control.add_argument(
|
|
150
|
+
"--filter-fwhm-mm",
|
|
151
|
+
type=float,
|
|
152
|
+
default=None,
|
|
153
|
+
help="Smoothing FWHM (mm) used when splicing repaired biharmonic-filter voxels back in. "
|
|
154
|
+
"Default: derived from the native MRSI voxel size (mean voxel dimension x sqrt(2)).",
|
|
155
|
+
)
|
|
156
|
+
processing_control.add_argument("--spikepc", type=float, default=99.0)
|
|
157
|
+
processing_control.add_argument("--no-pvc", action="store_true")
|
|
158
|
+
processing_control.add_argument(
|
|
159
|
+
"--longitudinal",
|
|
160
|
+
action="store_true",
|
|
161
|
+
help="Build one unbiased ANTs template across a subject's sessions and register it to MNI once, "
|
|
162
|
+
"composing (session-to-template)+(template-to-MNI) instead of registering each session directly to MNI. "
|
|
163
|
+
"No-op for subjects with a single session. Requires --registration-backend ants.",
|
|
164
|
+
)
|
|
165
|
+
processing_control.add_argument("--transform-spikemask", action="store_true", help="Also transform per-metabolite spike masks into T1w/MNI space.")
|
|
166
|
+
processing_control.add_argument("--nthreads", type=int, default=16, help="ANTs/ITK thread count per subject/session process.")
|
|
167
|
+
processing_control.add_argument(
|
|
168
|
+
"--nproc",
|
|
169
|
+
type=int,
|
|
170
|
+
default=1,
|
|
171
|
+
help="Number of subject/session recordings to process in parallel. Each parallel process gets --nthreads threads; "
|
|
172
|
+
"if nproc*nthreads exceeds the available CPU count, --nthreads is coerced down and a warning is shown at startup.",
|
|
173
|
+
)
|
|
174
|
+
processing_control.add_argument("--work-dir", "-w", type=Path, default=None)
|
|
175
|
+
|
|
176
|
+
overwrite = parser.add_argument_group("overwrite/recompute")
|
|
177
|
+
overwrite.add_argument("--overwrite", action="store_true")
|
|
178
|
+
overwrite.add_argument("--overwrite-filt", action="store_true")
|
|
179
|
+
overwrite.add_argument("--overwrite-seg", action="store_true", help="Force recompute of tissue segmentation (SynthSeg brain extraction + dseg/probseg), even if cached outputs exist.")
|
|
180
|
+
overwrite.add_argument("--overwrite-pve", action="store_true")
|
|
181
|
+
overwrite.add_argument("--overwrite-t1-reg", action="store_true")
|
|
182
|
+
overwrite.add_argument("--overwrite-mni-reg", action="store_true")
|
|
183
|
+
overwrite.add_argument("--overwrite-transform", action="store_true")
|
|
184
|
+
overwrite.add_argument("--overwrite-chimera", action="store_true", help="Force re-run Chimera parcellation even if the output dseg file already exists.")
|
|
185
|
+
|
|
186
|
+
runtime = parser.add_argument_group("Other options")
|
|
187
|
+
runtime.add_argument("--validate-only", action="store_true", help="Check selected subject/session inputs and exit without running preprocessing.")
|
|
188
|
+
runtime.add_argument(
|
|
189
|
+
"--skip-file-integrity-check",
|
|
190
|
+
action="store_true",
|
|
191
|
+
help="Skip forcing a full read of T1w/MRSI input files during preflight validation (existence-only checks instead). "
|
|
192
|
+
"By default every run force-reads these files first and skips any recording with a missing or corrupt/truncated input.",
|
|
193
|
+
)
|
|
194
|
+
runtime.add_argument("--check-external-libs", action="store_true", help="Verify required external binaries are available and exit.")
|
|
195
|
+
runtime.add_argument(
|
|
196
|
+
"--stop-on-first-crash",
|
|
197
|
+
action="store_true",
|
|
198
|
+
help="Abort the whole run immediately on the first recording failure, instead of logging it and continuing with the rest of the batch.",
|
|
199
|
+
)
|
|
200
|
+
runtime.add_argument(
|
|
201
|
+
"--verbose",
|
|
202
|
+
"-v",
|
|
203
|
+
type=int,
|
|
204
|
+
choices=[0, 1, 2, 3],
|
|
205
|
+
default=1,
|
|
206
|
+
help="0=subject start/finish only, 1=+processing steps, 2=+step details, 3=+raw ANTs/recon-all/mri_synthseg output.",
|
|
207
|
+
)
|
|
208
|
+
return parser
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def parse_args(argv: list[str] | None = None) -> MRSIPrepConfig:
|
|
212
|
+
args = build_parser().parse_args(argv)
|
|
213
|
+
return MRSIPrepConfig(
|
|
214
|
+
bids_dir=args.bids_dir,
|
|
215
|
+
output_dir=args.output_dir,
|
|
216
|
+
analysis_level=args.analysis_level,
|
|
217
|
+
participant_label=args.participant_label,
|
|
218
|
+
session_label=args.session_label,
|
|
219
|
+
participants_file=args.participants,
|
|
220
|
+
bids_filter_file=args.bids_filter_file,
|
|
221
|
+
metabolites=args.metabolites,
|
|
222
|
+
quality_metrics=args.quality_metrics,
|
|
223
|
+
snr_min=args.snr_min,
|
|
224
|
+
linewidth_max=args.linewidth_max,
|
|
225
|
+
crlb_max=args.crlb_max,
|
|
226
|
+
processing_mode=args.processing_mode,
|
|
227
|
+
tissue_backend=args.tissue_backend,
|
|
228
|
+
registration_backend=args.registration_backend,
|
|
229
|
+
ants_mrsi_to_t1_transform=args.ants_mrsi_to_t1_transform,
|
|
230
|
+
ants_t1_to_mni_transform=args.ants_t1_to_mni_transform,
|
|
231
|
+
fsl_mrsi_to_t1_dof=args.fsl_mrsi_to_t1_dof,
|
|
232
|
+
fsl_mrsi_to_t1_init=args.fsl_mrsi_to_t1_init,
|
|
233
|
+
fsl_t1_to_mni_dof=args.fsl_t1_to_mni_dof,
|
|
234
|
+
fsl_cost=args.fsl_cost,
|
|
235
|
+
normalization=args.normalization,
|
|
236
|
+
output_spaces=args.output_spaces,
|
|
237
|
+
output_mrsi_t1w=args.output_mrsi_t1w,
|
|
238
|
+
mni_resolution=args.mni_resolution,
|
|
239
|
+
registration_t1_target=args.registration_t1_target,
|
|
240
|
+
csf_pv_threshold=args.csf_pv_threshold,
|
|
241
|
+
ref_met=args.ref_met,
|
|
242
|
+
t1_pattern=args.t1_pattern,
|
|
243
|
+
parcellation_mode=args.parcellation_mode,
|
|
244
|
+
synthseg_mode=args.synthseg_mode,
|
|
245
|
+
chimera_scheme=args.chimera_scheme,
|
|
246
|
+
chimera_scale=args.chimera_scale,
|
|
247
|
+
chimera_grow=args.chimera_grow,
|
|
248
|
+
atlas=args.atlas,
|
|
249
|
+
custom_atlas=args.custom_atlas,
|
|
250
|
+
custom_atlas_lut=args.custom_atlas_lut,
|
|
251
|
+
fs_subjects_dir=args.fs_subjects_dir,
|
|
252
|
+
write_connectivity=args.write_connectivity,
|
|
253
|
+
connectivity_method=args.connectivity_method,
|
|
254
|
+
connectivity_space=args.connectivity_space,
|
|
255
|
+
connectivity_n_perturbations=args.connectivity_n_perturbations,
|
|
256
|
+
connectivity_sigma_scale=args.connectivity_sigma_scale,
|
|
257
|
+
connectivity_exclude_parcels=args.connectivity_exclude_parcels,
|
|
258
|
+
connectivity_max_parcel_id=args.connectivity_max_parcel_id,
|
|
259
|
+
regional_summary=args.regional_summary,
|
|
260
|
+
transform=args.transform,
|
|
261
|
+
filter_biharmonic=not args.no_filter,
|
|
262
|
+
filter_fwhm_mm=args.filter_fwhm_mm,
|
|
263
|
+
spike_percentile=args.spikepc,
|
|
264
|
+
no_pvc=args.no_pvc,
|
|
265
|
+
longitudinal=args.longitudinal,
|
|
266
|
+
transform_spikemask=args.transform_spikemask,
|
|
267
|
+
nthreads=args.nthreads,
|
|
268
|
+
nproc=args.nproc,
|
|
269
|
+
work_dir=args.work_dir,
|
|
270
|
+
overwrite=args.overwrite,
|
|
271
|
+
overwrite_filt=args.overwrite_filt,
|
|
272
|
+
overwrite_seg=args.overwrite_seg,
|
|
273
|
+
overwrite_pve=args.overwrite_pve,
|
|
274
|
+
overwrite_t1_reg=args.overwrite_t1_reg,
|
|
275
|
+
overwrite_mni_reg=args.overwrite_mni_reg,
|
|
276
|
+
overwrite_transform=args.overwrite_transform,
|
|
277
|
+
overwrite_chimera=args.overwrite_chimera,
|
|
278
|
+
validate_only=args.validate_only,
|
|
279
|
+
skip_file_integrity_check=args.skip_file_integrity_check,
|
|
280
|
+
check_external_libs=args.check_external_libs,
|
|
281
|
+
stop_on_first_crash=args.stop_on_first_crash,
|
|
282
|
+
verbose=args.verbose,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _parse_comma_list(value: str) -> list[str]:
|
|
287
|
+
return [item.strip() for item in str(value).split(",") if item.strip()]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _parse_scale(value) -> int:
|
|
291
|
+
text = str(value)
|
|
292
|
+
if text.startswith("scale"):
|
|
293
|
+
text = text[len("scale") :]
|
|
294
|
+
return int(text)
|
mrsiprep/cli/run.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""MRSIPrep command entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from mrsiprep.cli.parser import parse_args
|
|
8
|
+
from mrsiprep.utils.banner import print_banner
|
|
9
|
+
from mrsiprep.utils.debug import Debug
|
|
10
|
+
from mrsiprep.utils.logging import setup_logging
|
|
11
|
+
from mrsiprep.utils.provenance import check_external_software
|
|
12
|
+
from mrsiprep.workflows.participant import run_participant_workflow, validate_participant_inputs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
print_banner()
|
|
17
|
+
config = parse_args(argv)
|
|
18
|
+
logger = setup_logging(config.verbose, log_dir=config.logs_dir)
|
|
19
|
+
nproc, nthreads, cpu_warning = config.resolve_cpu_budget()
|
|
20
|
+
if cpu_warning:
|
|
21
|
+
logger.warning(cpu_warning)
|
|
22
|
+
config.nproc, config.nthreads = nproc, nthreads
|
|
23
|
+
if config.analysis_level != "participant":
|
|
24
|
+
logger.error("Only participant analysis level is currently supported.")
|
|
25
|
+
return 2
|
|
26
|
+
if config.check_external_libs:
|
|
27
|
+
debug = Debug(verbose=config.verbose)
|
|
28
|
+
ok = check_external_software(debug, config)
|
|
29
|
+
return 0 if ok else 1
|
|
30
|
+
if config.validate_only:
|
|
31
|
+
statuses = validate_participant_inputs(config)
|
|
32
|
+
failed = [status for status in statuses if status.status != "success"]
|
|
33
|
+
succeeded = [status for status in statuses if status.status == "success"]
|
|
34
|
+
logger.info("MRSIPrep input validation finished: %d valid, %d invalid", len(succeeded), len(failed))
|
|
35
|
+
for status in failed:
|
|
36
|
+
logger.error("INVALID sub-%s%s: %s", status.subject, f" ses-{status.session}" if status.session else "", status.error)
|
|
37
|
+
return 1 if failed else 0
|
|
38
|
+
statuses = run_participant_workflow(config)
|
|
39
|
+
failed = [status for status in statuses if status.status != "success"]
|
|
40
|
+
succeeded = [status for status in statuses if status.status == "success"]
|
|
41
|
+
logger.info("MRSIPrep finished: %d succeeded, %d failed", len(succeeded), len(failed))
|
|
42
|
+
for status in failed:
|
|
43
|
+
logger.error("FAILED sub-%s%s: %s", status.subject, f" ses-{status.session}" if status.session else "", status.error)
|
|
44
|
+
return 1 if failed and not succeeded else 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Default values for MRSIPrep."""
|
|
2
|
+
|
|
3
|
+
METABOLITE_ALIASES = {
|
|
4
|
+
"Glx": ["GluGln", "Glx"],
|
|
5
|
+
"GluGln": ["GluGln", "Glx"],
|
|
6
|
+
"NAA": ["NAA", "NAANAAG"],
|
|
7
|
+
"tNAA": ["NAANAAG", "tNAA", "NAA"],
|
|
8
|
+
"NAANAAG": ["NAANAAG", "tNAA", "NAA"],
|
|
9
|
+
"Cho": ["GPCPCh", "Cho"],
|
|
10
|
+
"GPCPCh": ["GPCPCh", "Cho"],
|
|
11
|
+
"tCr": ["CrPCr", "tCr"],
|
|
12
|
+
"CrPCr": ["CrPCr", "tCr"],
|
|
13
|
+
"Ins": ["Ins"],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
QUALITY_DEFAULTS = {
|
|
17
|
+
"snr_min": 4.0,
|
|
18
|
+
"linewidth_max": 0.1,
|
|
19
|
+
"crlb_max": 20.0,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
CHIMERA_SCALES = {"scale1": 1, "scale2": 2, "scale3": 3, "scale4": 4, "scale5": 5}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Runtime configuration objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .defaults import QUALITY_DEFAULTS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class MRSIPrepConfig:
|
|
13
|
+
bids_dir: Path
|
|
14
|
+
output_dir: Path
|
|
15
|
+
analysis_level: str
|
|
16
|
+
participant_label: list[str] = field(default_factory=list)
|
|
17
|
+
session_label: list[str] = field(default_factory=list)
|
|
18
|
+
participants_file: Path | None = None
|
|
19
|
+
bids_filter_file: Path | None = None
|
|
20
|
+
metabolites: list[str] | None = None
|
|
21
|
+
quality_metrics: list[str] = field(default_factory=lambda: ["snr", "linewidth", "crlb"])
|
|
22
|
+
snr_min: float = QUALITY_DEFAULTS["snr_min"]
|
|
23
|
+
linewidth_max: float = QUALITY_DEFAULTS["linewidth_max"]
|
|
24
|
+
crlb_max: float = QUALITY_DEFAULTS["crlb_max"]
|
|
25
|
+
processing_mode: str = "mni-norm"
|
|
26
|
+
tissue_backend: str = "synthseg-fast"
|
|
27
|
+
registration_backend: str = "ants"
|
|
28
|
+
ants_mrsi_to_t1_transform: str = "sr"
|
|
29
|
+
ants_t1_to_mni_transform: str = "s"
|
|
30
|
+
fsl_mrsi_to_t1_dof: int = 6
|
|
31
|
+
fsl_mrsi_to_t1_init: str = "flirt"
|
|
32
|
+
fsl_t1_to_mni_dof: int = 12
|
|
33
|
+
fsl_cost: str = "mutualinfo"
|
|
34
|
+
normalization: str = "simple"
|
|
35
|
+
output_spaces: list[str] = field(default_factory=lambda: ["MNI152NLin2009cAsym"])
|
|
36
|
+
output_mrsi_t1w: bool = False
|
|
37
|
+
mni_resolution: str = "t1wres"
|
|
38
|
+
registration_t1_target: str | None = None
|
|
39
|
+
csf_pv_threshold: float = 0.95
|
|
40
|
+
parcellation_mode: str | None = None
|
|
41
|
+
synthseg_mode: str = "robust"
|
|
42
|
+
chimera_scheme: str = "LFMIHIFIS"
|
|
43
|
+
chimera_scale: int = 3
|
|
44
|
+
chimera_grow: int = 2
|
|
45
|
+
atlas: str = "chimera-LFMIHIFIS-3"
|
|
46
|
+
custom_atlas: Path | None = None
|
|
47
|
+
custom_atlas_lut: Path | None = None
|
|
48
|
+
fs_subjects_dir: Path | None = None
|
|
49
|
+
write_connectivity: bool = False
|
|
50
|
+
connectivity_method: str = "spearman"
|
|
51
|
+
connectivity_space: str = "MRSI"
|
|
52
|
+
connectivity_n_perturbations: int = 50
|
|
53
|
+
connectivity_sigma_scale: float = 2.0
|
|
54
|
+
connectivity_exclude_parcels: str | None = None
|
|
55
|
+
connectivity_max_parcel_id: int | None = None
|
|
56
|
+
regional_summary: str = "mean"
|
|
57
|
+
nthreads: int = 16
|
|
58
|
+
nproc: int = 1
|
|
59
|
+
ref_met: str | None = None
|
|
60
|
+
t1_pattern: str = "desc-brain_T1w"
|
|
61
|
+
transform: str = ""
|
|
62
|
+
filter_biharmonic: bool = True
|
|
63
|
+
filter_fwhm_mm: float | None = None
|
|
64
|
+
spike_percentile: float = 99.0
|
|
65
|
+
no_pvc: bool = False
|
|
66
|
+
longitudinal: bool = False
|
|
67
|
+
transform_spikemask: bool = False
|
|
68
|
+
overwrite: bool = False
|
|
69
|
+
overwrite_filt: bool = False
|
|
70
|
+
overwrite_seg: bool = False
|
|
71
|
+
overwrite_pve: bool = False
|
|
72
|
+
overwrite_t1_reg: bool = False
|
|
73
|
+
overwrite_mni_reg: bool = False
|
|
74
|
+
overwrite_transform: bool = False
|
|
75
|
+
overwrite_chimera: bool = False
|
|
76
|
+
work_dir: Path | None = None
|
|
77
|
+
verbose: int = 1
|
|
78
|
+
validate_only: bool = False
|
|
79
|
+
skip_file_integrity_check: bool = False
|
|
80
|
+
check_external_libs: bool = False
|
|
81
|
+
stop_on_first_crash: bool = False
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
if not self.metabolites:
|
|
85
|
+
raise ValueError("--metabolites is required (comma-separated list, e.g. 'CrPCr,GluGln,GPCPCh,NAANAAG,Ins').")
|
|
86
|
+
if not self.ref_met:
|
|
87
|
+
raise ValueError("--ref-met is required (reference metabolite used to build the MRSI registration target).")
|
|
88
|
+
self.bids_dir = Path(self.bids_dir).resolve()
|
|
89
|
+
self.output_dir = Path(self.output_dir).resolve()
|
|
90
|
+
if self.bids_filter_file is not None:
|
|
91
|
+
self.bids_filter_file = Path(self.bids_filter_file).resolve()
|
|
92
|
+
from mrsiprep.io.bids import load_bids_filters
|
|
93
|
+
|
|
94
|
+
self.bids_filters = load_bids_filters(self.bids_filter_file)
|
|
95
|
+
self.output_spaces = _normalize_output_spaces(self.output_spaces)
|
|
96
|
+
if self.work_dir is None:
|
|
97
|
+
self.work_dir = self.output_dir / "work"
|
|
98
|
+
else:
|
|
99
|
+
self.work_dir = Path(self.work_dir).resolve()
|
|
100
|
+
if self.fs_subjects_dir is not None:
|
|
101
|
+
self.fs_subjects_dir = Path(self.fs_subjects_dir).resolve()
|
|
102
|
+
if self.processing_mode not in {"mni-norm", "parc-con", "midas"}:
|
|
103
|
+
raise ValueError(f"Unsupported processing mode: {self.processing_mode}")
|
|
104
|
+
if self.synthseg_mode not in {"fast", "standard", "robust"}:
|
|
105
|
+
raise ValueError(f"Unsupported SynthSeg mode: {self.synthseg_mode}")
|
|
106
|
+
if self.tissue_backend not in {"synthseg-fast", "existing", "none"}:
|
|
107
|
+
raise ValueError(f"Unsupported tissue backend: {self.tissue_backend}")
|
|
108
|
+
if self.registration_backend in {"flirt/fnirt", "flirt_fnirt", "flirt-fnirt"}:
|
|
109
|
+
self.registration_backend = "fsl"
|
|
110
|
+
if self.registration_backend not in {"ants", "fsl"}:
|
|
111
|
+
raise ValueError(f"Unsupported registration backend: {self.registration_backend}")
|
|
112
|
+
if self.registration_backend == "fsl" and self.longitudinal:
|
|
113
|
+
raise ValueError("--longitudinal currently requires --registration-backend ants.")
|
|
114
|
+
if self.fsl_mrsi_to_t1_dof not in {6, 7, 9, 12}:
|
|
115
|
+
raise ValueError("--fsl-mrsi-to-t1-dof must be one of 6, 7, 9, or 12.")
|
|
116
|
+
if self.fsl_mrsi_to_t1_init not in {"flirt", "usesqform"}:
|
|
117
|
+
raise ValueError("--fsl-mrsi-to-t1-init must be 'flirt' or 'usesqform'.")
|
|
118
|
+
if self.fsl_t1_to_mni_dof not in {6, 7, 9, 12}:
|
|
119
|
+
raise ValueError("--fsl-t1-to-mni-dof must be one of 6, 7, 9, or 12.")
|
|
120
|
+
if self.tissue_backend == "none":
|
|
121
|
+
self.no_pvc = True
|
|
122
|
+
if self.registration_t1_target is None:
|
|
123
|
+
self.registration_t1_target = "brain" if self.processing_mode in {"mni-norm", "midas"} else "brain-csf"
|
|
124
|
+
if self.parcellation_mode is None:
|
|
125
|
+
# midas defaults to SynthSeg parcellation: subject-native, needs no
|
|
126
|
+
# recon-all, and its GM/WM atlas suffices for the Eq. 4 regression.
|
|
127
|
+
self.parcellation_mode = "synthseg" if self.processing_mode in {"mni-norm", "midas"} else "chimera"
|
|
128
|
+
if self.processing_mode == "mni-norm" and self.parcellation_mode != "synthseg":
|
|
129
|
+
raise ValueError("mni-norm only supports SynthSeg parcellation. Use --mode parc-con for Chimera or MNI atlases.")
|
|
130
|
+
if self.processing_mode == "mni-norm" and self.registration_t1_target not in {"brain", "raw"}:
|
|
131
|
+
raise ValueError("mni-norm supports SynthSeg brain or raw T1w registration targets.")
|
|
132
|
+
if self.processing_mode == "parc-con" and self.parcellation_mode == "synthseg":
|
|
133
|
+
raise ValueError("parc-con requires Chimera or MNI atlas parcellation.")
|
|
134
|
+
if self.processing_mode == "midas":
|
|
135
|
+
# MIDAS mode's tissue correction is the per-parcel Eq. 4 regression,
|
|
136
|
+
# not PETPVC RBV; the paper has no voxelwise PVC step. Fuzzy c-means
|
|
137
|
+
# always supplies its own GM/WM/CSF maps, so the SynthSeg+FAST/CAT12
|
|
138
|
+
# tissue backends do not apply here.
|
|
139
|
+
self.no_pvc = True
|
|
140
|
+
self.tissue_backend = "synthseg-fast"
|
|
141
|
+
self.nproc = max(1, int(self.nproc))
|
|
142
|
+
self.nthreads = max(1, int(self.nthreads))
|
|
143
|
+
|
|
144
|
+
def resolve_cpu_budget(self) -> tuple[int, int, str | None]:
|
|
145
|
+
"""Coerce nproc*nthreads to the available CPU count.
|
|
146
|
+
|
|
147
|
+
Returns (nproc, nthreads, warning) where warning is set (and nthreads
|
|
148
|
+
reduced) if the requested total thread budget exceeds the machine's
|
|
149
|
+
CPU count.
|
|
150
|
+
"""
|
|
151
|
+
import os
|
|
152
|
+
|
|
153
|
+
cpu_count = os.cpu_count() or 1
|
|
154
|
+
requested_total = self.nproc * self.nthreads
|
|
155
|
+
if requested_total <= cpu_count:
|
|
156
|
+
return self.nproc, self.nthreads, None
|
|
157
|
+
coerced_nthreads = max(1, cpu_count // self.nproc)
|
|
158
|
+
warning = (
|
|
159
|
+
f"--nproc {self.nproc} x --nthreads {self.nthreads} = {requested_total} threads exceeds "
|
|
160
|
+
f"{cpu_count} available CPUs; coercing --nthreads to {coerced_nthreads} "
|
|
161
|
+
f"({self.nproc} x {coerced_nthreads} = {self.nproc * coerced_nthreads})."
|
|
162
|
+
)
|
|
163
|
+
return self.nproc, coerced_nthreads, warning
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def derivative_dir(self) -> Path:
|
|
167
|
+
return self.output_dir if self.output_dir.name == "mrsiprep" else self.output_dir / "mrsiprep"
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def source_derivatives_dir(self) -> Path:
|
|
171
|
+
return self.bids_dir / "derivatives"
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def logs_dir(self) -> Path:
|
|
175
|
+
return self.derivative_dir / "logs"
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def freesurfer_dir(self) -> Path:
|
|
179
|
+
if self.fs_subjects_dir is not None:
|
|
180
|
+
return self.fs_subjects_dir
|
|
181
|
+
return self.output_dir / "freesurfer"
|
|
182
|
+
|
|
183
|
+
def to_dict(self) -> dict:
|
|
184
|
+
out = asdict(self)
|
|
185
|
+
for key, value in list(out.items()):
|
|
186
|
+
if isinstance(value, Path):
|
|
187
|
+
out[key] = str(value)
|
|
188
|
+
elif isinstance(value, list):
|
|
189
|
+
out[key] = [str(item) if isinstance(item, Path) else item for item in value]
|
|
190
|
+
return out
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _normalize_output_spaces(spaces: list[str]) -> list[str]:
|
|
194
|
+
aliases = {
|
|
195
|
+
"mrsi": "MRSI",
|
|
196
|
+
"orig": "MRSI",
|
|
197
|
+
"t1": "T1w",
|
|
198
|
+
"t1w": "T1w",
|
|
199
|
+
"mni": "MNI152NLin2009cAsym",
|
|
200
|
+
"mni152": "MNI152NLin2009cAsym",
|
|
201
|
+
"mni152nlin2009casym": "MNI152NLin2009cAsym",
|
|
202
|
+
}
|
|
203
|
+
normalized = []
|
|
204
|
+
for value in spaces:
|
|
205
|
+
key = str(value).strip().lower()
|
|
206
|
+
if key not in aliases:
|
|
207
|
+
supported = ", ".join(sorted(aliases))
|
|
208
|
+
raise ValueError(f"Unsupported output space '{value}'. Supported values: {supported}")
|
|
209
|
+
canonical = aliases[key]
|
|
210
|
+
if canonical not in normalized:
|
|
211
|
+
normalized.append(canonical)
|
|
212
|
+
return normalized
|