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.
Files changed (92) hide show
  1. mrsiprep/cli/parser.py +294 -0
  2. mrsiprep/cli/run.py +48 -0
  3. mrsiprep/config/defaults.py +22 -0
  4. mrsiprep/config/settings.py +212 -0
  5. mrsiprep/connectivity/connectivity.py +228 -0
  6. mrsiprep/connectivity/edges.py +17 -0
  7. mrsiprep/connectivity/export.py +92 -0
  8. mrsiprep/connectivity/nodes.py +22 -0
  9. mrsiprep/data/atlas/README.md +12 -0
  10. mrsiprep/data/atlas/chimera-LFMIHIFIS-2/chimera-LFMIHIFIS-2.nii.gz +0 -0
  11. mrsiprep/data/atlas/chimera-LFMIHIFIS-2/chimera-LFMIHIFIS-2.tsv +689 -0
  12. mrsiprep/data/atlas/chimera-LFMIHIFIS-3/chimera-LFMIHIFIS-3.nii.gz +0 -0
  13. mrsiprep/data/atlas/chimera-LFMIHIFIS-3/chimera-LFMIHIFIS-3.tsv +277 -0
  14. mrsiprep/data/atlas/chimera-LFMIHISIFF-3/chimera-LFMIHISIFF-3.nii.gz +0 -0
  15. mrsiprep/data/atlas/chimera-LFMIHISIFF-3/chimera-LFMIHISIFF-3.tsv +538 -0
  16. mrsiprep/interfaces/ants.py +362 -0
  17. mrsiprep/interfaces/bids_import.py +268 -0
  18. mrsiprep/interfaces/bids_import_gui.py +41 -0
  19. mrsiprep/interfaces/chimera.py +112 -0
  20. mrsiprep/interfaces/freesurfer.py +96 -0
  21. mrsiprep/interfaces/fsl.py +185 -0
  22. mrsiprep/io/bids.py +308 -0
  23. mrsiprep/io/derivatives.py +26 -0
  24. mrsiprep/io/loaders.py +34 -0
  25. mrsiprep/io/naming.py +163 -0
  26. mrsiprep/io/validators.py +76 -0
  27. mrsiprep/mrsi/filtering.py +97 -0
  28. mrsiprep/mrsi/masks.py +32 -0
  29. mrsiprep/mrsi/pvc.py +63 -0
  30. mrsiprep/mrsi/quality.py +63 -0
  31. mrsiprep/mrsi/reference.py +31 -0
  32. mrsiprep/mrsi/resampling.py +92 -0
  33. mrsiprep/parcellation/atlas_registry.py +97 -0
  34. mrsiprep/parcellation/base.py +18 -0
  35. mrsiprep/parcellation/chimera_native.py +73 -0
  36. mrsiprep/parcellation/extraction.py +95 -0
  37. mrsiprep/parcellation/labels.py +65 -0
  38. mrsiprep/parcellation/metprofiles.py +97 -0
  39. mrsiprep/parcellation/mni_atlas.py +32 -0
  40. mrsiprep/parcellation/parcel_fractions.py +18 -0
  41. mrsiprep/parcellation/synthseg.py +112 -0
  42. mrsiprep/parcellation/tissue_regression.py +174 -0
  43. mrsiprep/registration/mrsi_to_t1.py +87 -0
  44. mrsiprep/registration/subject_template.py +148 -0
  45. mrsiprep/registration/t1_to_mni.py +82 -0
  46. mrsiprep/registration/transforms.py +73 -0
  47. mrsiprep/reports/connectivity_overview.py +44 -0
  48. mrsiprep/reports/coverage.py +12 -0
  49. mrsiprep/reports/figures.py +13 -0
  50. mrsiprep/reports/html.py +87 -0
  51. mrsiprep/reports/mrsi_preproc.py +64 -0
  52. mrsiprep/reports/parcel_figures.py +145 -0
  53. mrsiprep/reports/parcel_qc.py +111 -0
  54. mrsiprep/reports/parcellation_overview.py +43 -0
  55. mrsiprep/reports/qc_combine.py +46 -0
  56. mrsiprep/reports/registration_overview.py +85 -0
  57. mrsiprep/reports/slices.py +113 -0
  58. mrsiprep/reports/spectra_qc.py +11 -0
  59. mrsiprep/reports/tissue.py +52 -0
  60. mrsiprep/tissue/correction.py +14 -0
  61. mrsiprep/tissue/fractions.py +49 -0
  62. mrsiprep/tissue/fuzzy_cmeans.py +132 -0
  63. mrsiprep/tissue/psf.py +168 -0
  64. mrsiprep/tissue/synthseg_fast.py +262 -0
  65. mrsiprep/utils/banner.py +18 -0
  66. mrsiprep/utils/debug.py +309 -0
  67. mrsiprep/utils/images.py +116 -0
  68. mrsiprep/utils/logging.py +50 -0
  69. mrsiprep/utils/misc.py +107 -0
  70. mrsiprep/utils/provenance.py +142 -0
  71. mrsiprep/utils/subprocess_utils.py +40 -0
  72. mrsiprep/utils/tables.py +25 -0
  73. mrsiprep/workflows/anatomical.py +111 -0
  74. mrsiprep/workflows/base.py +10 -0
  75. mrsiprep/workflows/connectivity.py +23 -0
  76. mrsiprep/workflows/mrsi.py +128 -0
  77. mrsiprep/workflows/nipype_engine/__init__.py +14 -0
  78. mrsiprep/workflows/nipype_engine/adapters.py +52 -0
  79. mrsiprep/workflows/nipype_engine/build.py +75 -0
  80. mrsiprep/workflows/nipype_engine/nodes.py +264 -0
  81. mrsiprep/workflows/nipype_engine/run.py +264 -0
  82. mrsiprep/workflows/parcellation.py +38 -0
  83. mrsiprep/workflows/participant.py +625 -0
  84. mrsiprep/workflows/registration.py +42 -0
  85. mrsiprep/workflows/reports.py +9 -0
  86. mrsiprep/workflows/tissue.py +68 -0
  87. mrsiprep-0.1.0.dist-info/METADATA +135 -0
  88. mrsiprep-0.1.0.dist-info/RECORD +92 -0
  89. mrsiprep-0.1.0.dist-info/WHEEL +5 -0
  90. mrsiprep-0.1.0.dist-info/entry_points.txt +4 -0
  91. mrsiprep-0.1.0.dist-info/licenses/LICENSE +57 -0
  92. 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