brkraw 0.3.11__py3-none-any.whl → 0.5.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.
- brkraw/__init__.py +9 -3
- brkraw/apps/__init__.py +12 -0
- brkraw/apps/addon/__init__.py +30 -0
- brkraw/apps/addon/core.py +35 -0
- brkraw/apps/addon/dependencies.py +402 -0
- brkraw/apps/addon/installation.py +500 -0
- brkraw/apps/addon/io.py +21 -0
- brkraw/apps/hook/__init__.py +25 -0
- brkraw/apps/hook/core.py +636 -0
- brkraw/apps/loader/__init__.py +10 -0
- brkraw/apps/loader/core.py +622 -0
- brkraw/apps/loader/formatter.py +288 -0
- brkraw/apps/loader/helper.py +797 -0
- brkraw/apps/loader/info/__init__.py +11 -0
- brkraw/apps/loader/info/scan.py +85 -0
- brkraw/apps/loader/info/scan.yaml +90 -0
- brkraw/apps/loader/info/study.py +69 -0
- brkraw/apps/loader/info/study.yaml +156 -0
- brkraw/apps/loader/info/transform.py +92 -0
- brkraw/apps/loader/types.py +220 -0
- brkraw/cli/__init__.py +5 -0
- brkraw/cli/commands/__init__.py +2 -0
- brkraw/cli/commands/addon.py +327 -0
- brkraw/cli/commands/config.py +205 -0
- brkraw/cli/commands/convert.py +903 -0
- brkraw/cli/commands/hook.py +348 -0
- brkraw/cli/commands/info.py +74 -0
- brkraw/cli/commands/init.py +214 -0
- brkraw/cli/commands/params.py +106 -0
- brkraw/cli/commands/prune.py +288 -0
- brkraw/cli/commands/session.py +371 -0
- brkraw/cli/hook_args.py +80 -0
- brkraw/cli/main.py +83 -0
- brkraw/cli/utils.py +60 -0
- brkraw/core/__init__.py +13 -0
- brkraw/core/config.py +380 -0
- brkraw/core/entrypoints.py +25 -0
- brkraw/core/formatter.py +367 -0
- brkraw/core/fs.py +495 -0
- brkraw/core/jcamp.py +600 -0
- brkraw/core/layout.py +451 -0
- brkraw/core/parameters.py +781 -0
- brkraw/core/zip.py +1121 -0
- brkraw/dataclasses/__init__.py +14 -0
- brkraw/dataclasses/node.py +139 -0
- brkraw/dataclasses/reco.py +33 -0
- brkraw/dataclasses/scan.py +61 -0
- brkraw/dataclasses/study.py +131 -0
- brkraw/default/__init__.py +3 -0
- brkraw/default/pruner_specs/deid4share.yaml +42 -0
- brkraw/default/rules/00_default.yaml +4 -0
- brkraw/default/specs/metadata_dicom.yaml +236 -0
- brkraw/default/specs/metadata_transforms.py +92 -0
- brkraw/resolver/__init__.py +7 -0
- brkraw/resolver/affine.py +539 -0
- brkraw/resolver/datatype.py +69 -0
- brkraw/resolver/fid.py +90 -0
- brkraw/resolver/helpers.py +36 -0
- brkraw/resolver/image.py +188 -0
- brkraw/resolver/nifti.py +370 -0
- brkraw/resolver/shape.py +235 -0
- brkraw/schema/__init__.py +3 -0
- brkraw/schema/context_map.yaml +62 -0
- brkraw/schema/meta.yaml +57 -0
- brkraw/schema/niftiheader.yaml +95 -0
- brkraw/schema/pruner.yaml +55 -0
- brkraw/schema/remapper.yaml +128 -0
- brkraw/schema/rules.yaml +154 -0
- brkraw/specs/__init__.py +10 -0
- brkraw/specs/hook/__init__.py +12 -0
- brkraw/specs/hook/logic.py +31 -0
- brkraw/specs/hook/validator.py +22 -0
- brkraw/specs/meta/__init__.py +5 -0
- brkraw/specs/meta/validator.py +156 -0
- brkraw/specs/pruner/__init__.py +15 -0
- brkraw/specs/pruner/logic.py +361 -0
- brkraw/specs/pruner/validator.py +119 -0
- brkraw/specs/remapper/__init__.py +27 -0
- brkraw/specs/remapper/logic.py +924 -0
- brkraw/specs/remapper/validator.py +314 -0
- brkraw/specs/rules/__init__.py +6 -0
- brkraw/specs/rules/logic.py +263 -0
- brkraw/specs/rules/validator.py +103 -0
- brkraw-0.5.0.dist-info/METADATA +81 -0
- brkraw-0.5.0.dist-info/RECORD +88 -0
- {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info}/WHEEL +1 -2
- brkraw-0.5.0.dist-info/entry_points.txt +13 -0
- brkraw/lib/__init__.py +0 -4
- brkraw/lib/backup.py +0 -641
- brkraw/lib/bids.py +0 -0
- brkraw/lib/errors.py +0 -125
- brkraw/lib/loader.py +0 -1220
- brkraw/lib/orient.py +0 -194
- brkraw/lib/parser.py +0 -48
- brkraw/lib/pvobj.py +0 -301
- brkraw/lib/reference.py +0 -245
- brkraw/lib/utils.py +0 -471
- brkraw/scripts/__init__.py +0 -0
- brkraw/scripts/brk_backup.py +0 -106
- brkraw/scripts/brkraw.py +0 -744
- brkraw/ui/__init__.py +0 -0
- brkraw/ui/config.py +0 -17
- brkraw/ui/main_win.py +0 -214
- brkraw/ui/previewer.py +0 -225
- brkraw/ui/scan_info.py +0 -72
- brkraw/ui/scan_list.py +0 -73
- brkraw/ui/subj_info.py +0 -128
- brkraw-0.3.11.dist-info/METADATA +0 -25
- brkraw-0.3.11.dist-info/RECORD +0 -28
- brkraw-0.3.11.dist-info/entry_points.txt +0 -3
- brkraw-0.3.11.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
"""Loader utilities that attach conversion helpers to Bruker scans.
|
|
2
|
+
|
|
3
|
+
This module binds helper methods onto Scan/Reco objects so callers can fetch
|
|
4
|
+
reconstructed data, affines, NIfTI images, metadata, and parameter search
|
|
5
|
+
results with a simple API.
|
|
6
|
+
|
|
7
|
+
Last updated: 2025-12-30
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from types import MethodType
|
|
13
|
+
import re
|
|
14
|
+
import logging
|
|
15
|
+
import sys
|
|
16
|
+
from functools import partial
|
|
17
|
+
from typing import TYPE_CHECKING, Optional, Tuple, Union, Any, Mapping, Callable, cast, List, Dict, Iterable, Literal
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from ...core import config as config_core
|
|
21
|
+
from ...core.config import resolve_root
|
|
22
|
+
from ...specs import hook as converter_core
|
|
23
|
+
from ...specs.pruner import prune_dataset_to_zip
|
|
24
|
+
from ...specs.rules import load_rules, select_rule_use
|
|
25
|
+
from ...dataclasses import Scan, Study
|
|
26
|
+
from .types import StudyLoader, ScanLoader, RecoLoader
|
|
27
|
+
from .formatter import format_info_tables
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("brkraw")
|
|
30
|
+
from . import info as info_resolver
|
|
31
|
+
from .helper import (
|
|
32
|
+
make_dir,
|
|
33
|
+
convert as _convert,
|
|
34
|
+
get_affine as _get_affine,
|
|
35
|
+
get_dataobj as _get_dataobj,
|
|
36
|
+
get_metadata as _get_metadata,
|
|
37
|
+
get_nifti1image as _get_nifti1image,
|
|
38
|
+
search_parameters as _search_parameters,
|
|
39
|
+
apply_converter_hook as _apply_converter_hook,
|
|
40
|
+
resolve_data_and_affine as _resolve_data_and_affine,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
import numpy as np
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from ...resolver.nifti import Nifti1HeaderContents
|
|
47
|
+
from .types import XYZUNIT, TUNIT, SubjectType, SubjectPose, InfoScope, AffineSpace
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BrukerLoader:
|
|
52
|
+
"""High-level entrypoint that resolves scans and exposes handy accessors."""
|
|
53
|
+
|
|
54
|
+
def __init__(self,
|
|
55
|
+
path: Union[str, Path],
|
|
56
|
+
affine_decimals: Optional[int] = None):
|
|
57
|
+
"""
|
|
58
|
+
Create a loader for a Bruker study rooted at `path`.
|
|
59
|
+
|
|
60
|
+
This resolves image/affine metadata for each available scan and binds
|
|
61
|
+
convenience methods (`get_dataobj`, `get_affine`) directly onto scan
|
|
62
|
+
instances for downstream use.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
path: Path to the study root.
|
|
66
|
+
affine_decimals: Decimal rounding applied to resolved affines.
|
|
67
|
+
"""
|
|
68
|
+
self._study: Union["Study", "StudyLoader"] = Study.from_path(path)
|
|
69
|
+
if affine_decimals is None:
|
|
70
|
+
affine_decimals = config_core.float_decimals(root=resolve_root(None))
|
|
71
|
+
self._affine_decimals = affine_decimals
|
|
72
|
+
self._sw_version: Optional[str] = self._parse_sw_version()
|
|
73
|
+
self._attach_helpers()
|
|
74
|
+
|
|
75
|
+
def _parse_sw_version(self) -> Optional[str]:
|
|
76
|
+
"""Resolve Paravision version from subject header or visu_pars."""
|
|
77
|
+
def _clean(value: object) -> Optional[str]:
|
|
78
|
+
if value is None:
|
|
79
|
+
return None
|
|
80
|
+
text = str(value).strip()
|
|
81
|
+
if text.startswith("<") and text.endswith(">"):
|
|
82
|
+
text = text[1:-1]
|
|
83
|
+
return text.strip()
|
|
84
|
+
|
|
85
|
+
def _parse_title(text: str) -> Optional[str]:
|
|
86
|
+
if not text:
|
|
87
|
+
return None
|
|
88
|
+
if text == "Parameter List":
|
|
89
|
+
return "5.1"
|
|
90
|
+
match = re.search(r"ParaVision\s+(\d+\.\d+\.\d+)", text)
|
|
91
|
+
if match:
|
|
92
|
+
return match.group(1)
|
|
93
|
+
match = re.search(r"ParaVision\s+360\s+V(\d+)\.(\d+)", text)
|
|
94
|
+
if match:
|
|
95
|
+
return f"360.{match.group(1)}.{match.group(2)}"
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def _parse_visu(text: str) -> Optional[str]:
|
|
99
|
+
if not text:
|
|
100
|
+
return None
|
|
101
|
+
match = re.search(r"(\d+\.\d+\.\d+)", text)
|
|
102
|
+
if match:
|
|
103
|
+
return match.group(1)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
study = self._study
|
|
107
|
+
try:
|
|
108
|
+
if getattr(study, "has_subject", False):
|
|
109
|
+
subject = getattr(study, "subject", None)
|
|
110
|
+
title = getattr(subject, "header", {}).get("TITLE")
|
|
111
|
+
cleaned = _clean(title)
|
|
112
|
+
parsed = _parse_title(cleaned or "")
|
|
113
|
+
if parsed:
|
|
114
|
+
return parsed
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
scan = next(iter(study.avail.values()))
|
|
120
|
+
reco = next(iter(scan.avail.values()))
|
|
121
|
+
from ...core.parameters import Parameters
|
|
122
|
+
|
|
123
|
+
visu_pars = cast(Parameters, reco.visu_pars)
|
|
124
|
+
value = None
|
|
125
|
+
try:
|
|
126
|
+
value = visu_pars["VisuCoreVersion"]
|
|
127
|
+
except Exception:
|
|
128
|
+
value = None
|
|
129
|
+
if value is None:
|
|
130
|
+
try:
|
|
131
|
+
value = visu_pars["VisuCreatorVersion"]
|
|
132
|
+
except Exception:
|
|
133
|
+
value = None
|
|
134
|
+
cleaned = _clean(value)
|
|
135
|
+
parsed = _parse_visu(cleaned or "")
|
|
136
|
+
return parsed
|
|
137
|
+
except Exception:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def _attach_helpers(self):
|
|
141
|
+
"""Resolve per-scan metadata and bind helper methods."""
|
|
142
|
+
self._study = cast(StudyLoader, self._study)
|
|
143
|
+
self._study.search_params = MethodType(_search_parameters, self._study)
|
|
144
|
+
base = resolve_root(None)
|
|
145
|
+
try:
|
|
146
|
+
rules = load_rules(root=base, validate=False)
|
|
147
|
+
except Exception:
|
|
148
|
+
rules = {}
|
|
149
|
+
for scan in self._study.avail.values():
|
|
150
|
+
_resolve_data_and_affine(
|
|
151
|
+
scan,
|
|
152
|
+
affine_decimals=self._affine_decimals
|
|
153
|
+
)
|
|
154
|
+
scan = cast(ScanLoader, scan)
|
|
155
|
+
|
|
156
|
+
# bind helper functions as methods on the scan instance
|
|
157
|
+
scan.get_dataobj = MethodType(_get_dataobj, scan)
|
|
158
|
+
scan.get_affine = MethodType(
|
|
159
|
+
partial(_get_affine, decimals=self._affine_decimals),
|
|
160
|
+
scan,
|
|
161
|
+
)
|
|
162
|
+
scan.get_nifti1image = MethodType(_get_nifti1image, scan)
|
|
163
|
+
scan.convert = MethodType(_convert, scan)
|
|
164
|
+
scan._converter_hook = None
|
|
165
|
+
scan._converter_hook_name = None
|
|
166
|
+
if rules:
|
|
167
|
+
try:
|
|
168
|
+
hook_name = select_rule_use(
|
|
169
|
+
scan,
|
|
170
|
+
rules.get("converter_hook", []),
|
|
171
|
+
base=base,
|
|
172
|
+
resolve_paths=False,
|
|
173
|
+
)
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
logger.debug(
|
|
176
|
+
"Converter hook rule selection failed for scan %s: %s",
|
|
177
|
+
getattr(scan, "scan_id", "?"),
|
|
178
|
+
exc,
|
|
179
|
+
exc_info=True,
|
|
180
|
+
)
|
|
181
|
+
hook_name = None
|
|
182
|
+
if isinstance(hook_name, str):
|
|
183
|
+
try:
|
|
184
|
+
entry = converter_core.resolve_hook(hook_name)
|
|
185
|
+
except Exception as exc:
|
|
186
|
+
logger.warning(
|
|
187
|
+
"Converter hook %r not available: %s",
|
|
188
|
+
hook_name,
|
|
189
|
+
exc,
|
|
190
|
+
)
|
|
191
|
+
entry = None
|
|
192
|
+
if entry:
|
|
193
|
+
logger.debug("Applying converter hook: %s", hook_name)
|
|
194
|
+
scan._converter_hook_name = hook_name
|
|
195
|
+
_apply_converter_hook(
|
|
196
|
+
scan,
|
|
197
|
+
entry,
|
|
198
|
+
affine_decimals=self._affine_decimals,
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
logger.debug("Converter hook %r resolved to no entry.", hook_name)
|
|
202
|
+
else:
|
|
203
|
+
logger.debug("No converter hook selected for scan %s.", getattr(scan, "scan_id", "?"))
|
|
204
|
+
scan.get_metadata = MethodType(_get_metadata, scan)
|
|
205
|
+
scan.search_params = MethodType(_search_parameters, scan)
|
|
206
|
+
for reco in scan.avail.values():
|
|
207
|
+
reco = cast(RecoLoader, reco)
|
|
208
|
+
reco.search_params = MethodType(_search_parameters, reco)
|
|
209
|
+
|
|
210
|
+
def search_params(self, key: str,
|
|
211
|
+
*,
|
|
212
|
+
file: Optional[Union[str, List[str]]] = None,
|
|
213
|
+
scan_id: Optional[int] = None,
|
|
214
|
+
reco_id: Optional[int] = None):
|
|
215
|
+
"""Search parameter files for keys on study/scan/reco objects.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
key: Parameter key to search for.
|
|
219
|
+
file: Filename or list of filenames to search.
|
|
220
|
+
scan_id: Scan id (required when searching from Study).
|
|
221
|
+
reco_id: Reco id (optional; flattens results for that reco).
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Mapping of filename to found values, or None if no hits.
|
|
225
|
+
"""
|
|
226
|
+
self._study = cast(StudyLoader, self._study)
|
|
227
|
+
return self._study.search_params(key, file=file, scan_id=scan_id, reco_id=reco_id)
|
|
228
|
+
|
|
229
|
+
def override_converter(
|
|
230
|
+
self,
|
|
231
|
+
scan_id: int,
|
|
232
|
+
converter_hook: Mapping[str, Callable[..., Any]],
|
|
233
|
+
) -> None:
|
|
234
|
+
"""Override scan conversion methods with a converter hook.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
scan_id: Scan identifier.
|
|
238
|
+
converter_hook: Mapping of method names to callables. Only
|
|
239
|
+
provided keys are overridden.
|
|
240
|
+
"""
|
|
241
|
+
scan = self.avail[scan_id]
|
|
242
|
+
scan = cast(ScanLoader, scan)
|
|
243
|
+
_apply_converter_hook(
|
|
244
|
+
scan,
|
|
245
|
+
converter_hook,
|
|
246
|
+
affine_decimals=self._affine_decimals,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def restore_converter(self, scan_id: int) -> None:
|
|
250
|
+
"""Restore default conversion methods for a scan.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
scan_id: Scan identifier.
|
|
254
|
+
"""
|
|
255
|
+
scan = self.avail[scan_id]
|
|
256
|
+
scan = cast(ScanLoader, scan)
|
|
257
|
+
scan.get_dataobj = MethodType(_get_dataobj, scan)
|
|
258
|
+
scan.get_affine = MethodType(
|
|
259
|
+
partial(_get_affine, decimals=self._affine_decimals),
|
|
260
|
+
scan,
|
|
261
|
+
)
|
|
262
|
+
scan.get_nifti1image = MethodType(_get_nifti1image, scan)
|
|
263
|
+
scan.convert = MethodType(_convert, scan)
|
|
264
|
+
scan._converter_hook = None
|
|
265
|
+
|
|
266
|
+
def get_scan(self, scan_id: int) -> "ScanLoader":
|
|
267
|
+
"""Return scan by id.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
scan_id: Scan identifier.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Scan loader instance.
|
|
274
|
+
"""
|
|
275
|
+
scan = self._study.get_scan(scan_id)
|
|
276
|
+
scan = cast(ScanLoader, scan)
|
|
277
|
+
return scan
|
|
278
|
+
|
|
279
|
+
def get_fid(
|
|
280
|
+
self,
|
|
281
|
+
scan_id: int,
|
|
282
|
+
buffer_start: Optional[int] = None,
|
|
283
|
+
buffer_size: Optional[int] = None,
|
|
284
|
+
*,
|
|
285
|
+
as_complex: bool = True,
|
|
286
|
+
) -> Optional["np.ndarray"]:
|
|
287
|
+
"""Return FID/rawdata for a scan.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
scan_id: Scan identifier.
|
|
291
|
+
buffer_start: Optional byte offset to start reading.
|
|
292
|
+
buffer_size: Optional number of bytes to read.
|
|
293
|
+
as_complex: If True, return complex samples (default: True).
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
NumPy array of samples.
|
|
297
|
+
"""
|
|
298
|
+
scan = self.get_scan(scan_id)
|
|
299
|
+
if not hasattr(scan, 'get_fid'):
|
|
300
|
+
return None
|
|
301
|
+
return scan.get_fid(buffer_start=buffer_start, buffer_size=buffer_size, as_complex=as_complex)
|
|
302
|
+
|
|
303
|
+
def get_dataobj(self, scan_id: int, reco_id: Optional[int] = None) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
|
|
304
|
+
"""Return reconstructed data for a scan/reco via attached helper.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
scan_id: Scan identifier.
|
|
308
|
+
reco_id: Reco identifier (defaults to the first available).
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Single ndarray when one slice pack exists; otherwise a tuple.
|
|
312
|
+
"""
|
|
313
|
+
scan = self.get_scan(scan_id)
|
|
314
|
+
return scan.get_dataobj(reco_id)
|
|
315
|
+
|
|
316
|
+
def get_affine(self,
|
|
317
|
+
scan_id: int,
|
|
318
|
+
reco_id: Optional[int] = None,
|
|
319
|
+
*,
|
|
320
|
+
space: AffineSpace = 'subject_ras',
|
|
321
|
+
override_subject_type: Optional[SubjectType] = None,
|
|
322
|
+
override_subject_pose: Optional[SubjectPose] = None,
|
|
323
|
+
decimals: Optional[int] = None
|
|
324
|
+
) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
|
|
325
|
+
"""Return affine(s) for a scan/reco via attached helper.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
scan_id: Scan identifier.
|
|
329
|
+
reco_id: Reco identifier (defaults to the first available).
|
|
330
|
+
space: Output affine space ("raw", "scanner", "subject_ras").
|
|
331
|
+
override_subject_type: Subject type override for subject view.
|
|
332
|
+
override_subject_pose: Subject pose override for subject view.
|
|
333
|
+
decimals: Optional decimal rounding applied to returned affines.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Single affine matrix when one slice pack exists; otherwise a tuple.
|
|
337
|
+
"""
|
|
338
|
+
scan = self.get_scan(scan_id)
|
|
339
|
+
decimals = decimals or self._affine_decimals
|
|
340
|
+
return scan.get_affine(reco_id,
|
|
341
|
+
space=space,
|
|
342
|
+
override_subject_pose=override_subject_pose,
|
|
343
|
+
override_subject_type=override_subject_type,
|
|
344
|
+
decimals=decimals)
|
|
345
|
+
|
|
346
|
+
def get_nifti1image(self, scan_id: int, reco_id: Optional[int] = None,
|
|
347
|
+
*,
|
|
348
|
+
space: AffineSpace = 'subject_ras',
|
|
349
|
+
override_header: Optional[Nifti1HeaderContents] = None,
|
|
350
|
+
override_subject_type: Optional[SubjectType] = None,
|
|
351
|
+
override_subject_pose: Optional[SubjectPose] = None,
|
|
352
|
+
flip_x: bool = False,
|
|
353
|
+
flatten_fg: bool = False,
|
|
354
|
+
xyz_units: XYZUNIT = 'mm',
|
|
355
|
+
t_units: TUNIT = 'sec'):
|
|
356
|
+
"""Return NIfTI image(s) for a scan/reco via attached helper.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
scan_id: Scan identifier.
|
|
360
|
+
reco_id: Reco identifier (defaults to the first available).
|
|
361
|
+
space: Output affine space ("raw", "scanner", "subject_ras").
|
|
362
|
+
override_header: Optional header values to apply.
|
|
363
|
+
override_subject_type: Subject type override for subject view.
|
|
364
|
+
override_subject_pose: Subject pose override for subject view.
|
|
365
|
+
flip_x: If True, set NIfTI header x-flip flag.
|
|
366
|
+
xyz_units: Spatial units for NIfTI header.
|
|
367
|
+
t_units: Temporal units for NIfTI header.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Single NIfTI image when one slice pack exists; otherwise a tuple.
|
|
371
|
+
"""
|
|
372
|
+
scan = self.get_scan(scan_id)
|
|
373
|
+
return scan.convert(
|
|
374
|
+
reco_id,
|
|
375
|
+
format="nifti",
|
|
376
|
+
space=space,
|
|
377
|
+
override_header=override_header,
|
|
378
|
+
override_subject_type=override_subject_type,
|
|
379
|
+
override_subject_pose=override_subject_pose,
|
|
380
|
+
flip_x=flip_x,
|
|
381
|
+
flatten_fg=flatten_fg,
|
|
382
|
+
xyz_units=xyz_units,
|
|
383
|
+
t_units=t_units,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def convert(
|
|
387
|
+
self,
|
|
388
|
+
scan_id: int,
|
|
389
|
+
reco_id: Optional[int] = None,
|
|
390
|
+
*,
|
|
391
|
+
format: Literal["nifti", "nifti1"] = "nifti",
|
|
392
|
+
space: AffineSpace = 'subject_ras',
|
|
393
|
+
override_header: Optional[Nifti1HeaderContents] = None,
|
|
394
|
+
override_subject_type: Optional[SubjectType] = None,
|
|
395
|
+
override_subject_pose: Optional[SubjectPose] = None,
|
|
396
|
+
flip_x: bool = False,
|
|
397
|
+
flatten_fg: bool = False,
|
|
398
|
+
xyz_units: XYZUNIT = "mm",
|
|
399
|
+
t_units: TUNIT = "sec",
|
|
400
|
+
hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
|
|
401
|
+
):
|
|
402
|
+
"""Convert a scan/reco to the requested output format."""
|
|
403
|
+
scan = self.get_scan(scan_id)
|
|
404
|
+
return scan.convert(
|
|
405
|
+
reco_id,
|
|
406
|
+
format=format,
|
|
407
|
+
space=space,
|
|
408
|
+
override_header=override_header,
|
|
409
|
+
override_subject_type=override_subject_type,
|
|
410
|
+
override_subject_pose=override_subject_pose,
|
|
411
|
+
flip_x=flip_x,
|
|
412
|
+
flatten_fg=flatten_fg,
|
|
413
|
+
xyz_units=xyz_units,
|
|
414
|
+
t_units=t_units,
|
|
415
|
+
hook_args_by_name=hook_args_by_name,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def get_metadata(
|
|
419
|
+
self,
|
|
420
|
+
scan_id: int,
|
|
421
|
+
reco_id: Optional[int] = None,
|
|
422
|
+
spec: Optional[Union[Mapping[str, Any], str, Path]] = None,
|
|
423
|
+
context_map: Optional[Union[str, Path]] = None,
|
|
424
|
+
return_spec: bool = False,
|
|
425
|
+
):
|
|
426
|
+
"""Return metadata for a scan/reco.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
scan_id: Scan identifier.
|
|
430
|
+
reco_id: Reco identifier (defaults to the first available).
|
|
431
|
+
spec: Optional spec mapping or spec file path.
|
|
432
|
+
context_map: Optional context map override.
|
|
433
|
+
return_spec: If True, return spec info alongside metadata.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Mapping of metadata fields, or None when no spec matches. When
|
|
437
|
+
return_spec is True, returns (metadata, spec_info).
|
|
438
|
+
"""
|
|
439
|
+
scan = self.get_scan(scan_id)
|
|
440
|
+
return scan.get_metadata(
|
|
441
|
+
reco_id=reco_id,
|
|
442
|
+
spec=spec,
|
|
443
|
+
context_map=context_map,
|
|
444
|
+
return_spec=return_spec,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def prune_to_zip(
|
|
448
|
+
self,
|
|
449
|
+
dest: Union[str, Path],
|
|
450
|
+
files: Iterable[str],
|
|
451
|
+
*,
|
|
452
|
+
mode: Literal["keep", "drop"] = "keep",
|
|
453
|
+
update_params: Optional[Mapping[str, Mapping[str, Optional[str]]]] = None,
|
|
454
|
+
add_root: bool = True,
|
|
455
|
+
root_name: Optional[str] = None,
|
|
456
|
+
) -> "BrukerLoader":
|
|
457
|
+
"""Create a pruned dataset zip and return a loader for it.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
dest: Destination zip path.
|
|
461
|
+
files: Filenames or relative paths used by the selection mode.
|
|
462
|
+
mode: "keep" to include only matching files, "drop" to exclude them.
|
|
463
|
+
update_params: Mapping or YAML path of {filename: {key: value}} JCAMP edits.
|
|
464
|
+
add_root: Whether to include a top-level root directory in the zip.
|
|
465
|
+
root_name: Override the root directory name when add_root is True.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Loader bound to the newly created pruned zip.
|
|
469
|
+
"""
|
|
470
|
+
source = self._study.fs.root
|
|
471
|
+
out_path = prune_dataset_to_zip(
|
|
472
|
+
source,
|
|
473
|
+
dest,
|
|
474
|
+
files=files,
|
|
475
|
+
mode=mode,
|
|
476
|
+
update_params=update_params,
|
|
477
|
+
add_root=add_root,
|
|
478
|
+
root_name=root_name,
|
|
479
|
+
)
|
|
480
|
+
return BrukerLoader(out_path, affine_decimals=self._affine_decimals)
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def avail(self) -> Mapping[int, Union["Scan", "ScanLoader"]]:
|
|
484
|
+
"""Available scans keyed by scan id."""
|
|
485
|
+
return self._study.avail
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def subject(self) -> Optional[Dict[str, Any]]:
|
|
489
|
+
"""Parsed study/subject info resolved from subject metadata.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Mapping with Study/Subject entries, or None if resolution fails.
|
|
493
|
+
"""
|
|
494
|
+
try:
|
|
495
|
+
return info_resolver.study(self)
|
|
496
|
+
except Exception:
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def sw_version(self) -> Optional[str]:
|
|
501
|
+
"""Resolved Paravision version string, if available."""
|
|
502
|
+
return self._sw_version
|
|
503
|
+
|
|
504
|
+
def info(
|
|
505
|
+
self,
|
|
506
|
+
scope: InfoScope = 'full',
|
|
507
|
+
*,
|
|
508
|
+
scan_id: Optional[Union[int, List[int]]] = None,
|
|
509
|
+
as_dict: bool = False,
|
|
510
|
+
scan_transpose: bool = True,
|
|
511
|
+
float_decimals: Optional[int] = None,
|
|
512
|
+
width: Optional[int] = None,
|
|
513
|
+
show_reco: bool = True,
|
|
514
|
+
):
|
|
515
|
+
"""Return study/scan summaries as a dict or formatted table.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
scope: "full", "study", or "scan".
|
|
519
|
+
scan_id: Optional scan id or list of scan ids to include.
|
|
520
|
+
as_dict: If True, return a mapping; otherwise print a table and return None.
|
|
521
|
+
scan_transpose: If True, render scan fields in a transposed layout.
|
|
522
|
+
float_decimals: Decimal precision for floats (defaults to config).
|
|
523
|
+
width: Output table width (defaults to config).
|
|
524
|
+
show_reco: If False, omit reco entries from scan info.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
Mapping of info data, or None when formatted output is printed.
|
|
528
|
+
"""
|
|
529
|
+
rules = {}
|
|
530
|
+
base = resolve_root(None)
|
|
531
|
+
scan_info: Dict[int, Any] = {}
|
|
532
|
+
if width is None:
|
|
533
|
+
width = config_core.output_width(root=base)
|
|
534
|
+
if float_decimals is None:
|
|
535
|
+
float_decimals = config_core.float_decimals(root=base)
|
|
536
|
+
try:
|
|
537
|
+
rules = load_rules(root=base, validate=False)
|
|
538
|
+
except Exception:
|
|
539
|
+
rules = {}
|
|
540
|
+
|
|
541
|
+
if scope in ['full', 'scan']:
|
|
542
|
+
if scan_id is None:
|
|
543
|
+
scan_ids = list(self.avail.keys())
|
|
544
|
+
elif isinstance(scan_id, list):
|
|
545
|
+
scan_ids = scan_id
|
|
546
|
+
else:
|
|
547
|
+
scan_ids = [scan_id]
|
|
548
|
+
for sid in scan_ids:
|
|
549
|
+
scan = cast(ScanLoader, self.avail[sid])
|
|
550
|
+
spec_path = None
|
|
551
|
+
if rules:
|
|
552
|
+
try:
|
|
553
|
+
spec_path = select_rule_use(
|
|
554
|
+
scan,
|
|
555
|
+
rules.get("info_spec", []),
|
|
556
|
+
base=base,
|
|
557
|
+
resolve_paths=True,
|
|
558
|
+
)
|
|
559
|
+
except Exception:
|
|
560
|
+
spec_path = None
|
|
561
|
+
|
|
562
|
+
if isinstance(spec_path, Path) and spec_path.exists():
|
|
563
|
+
scan_info[sid] = info_resolver.scan(scan, spec_source=spec_path)
|
|
564
|
+
else:
|
|
565
|
+
scan_info[sid] = info_resolver.scan(scan)
|
|
566
|
+
|
|
567
|
+
if not show_reco and isinstance(scan_info[sid], dict):
|
|
568
|
+
scan_info[sid].pop("Reco(s)", None)
|
|
569
|
+
|
|
570
|
+
if scope == 'scan':
|
|
571
|
+
if not as_dict:
|
|
572
|
+
config_core.configure_logging(root=base, stream=sys.stdout)
|
|
573
|
+
text = format_info_tables(
|
|
574
|
+
{"Scan(s)": scan_info},
|
|
575
|
+
width=width,
|
|
576
|
+
scan_indent=1,
|
|
577
|
+
reco_indent=1,
|
|
578
|
+
scan_transpose=scan_transpose,
|
|
579
|
+
float_decimals=float_decimals,
|
|
580
|
+
)
|
|
581
|
+
logger.info("%s", text)
|
|
582
|
+
return None
|
|
583
|
+
return scan_info
|
|
584
|
+
|
|
585
|
+
study_info = dict(self.subject) if self.subject else {}
|
|
586
|
+
if self.sw_version:
|
|
587
|
+
study_block = dict(study_info.get("Study", {}))
|
|
588
|
+
study_block = {"Software": f"Paravision v{self.sw_version}", **study_block}
|
|
589
|
+
study_info["Study"] = study_block
|
|
590
|
+
|
|
591
|
+
if scope == 'study':
|
|
592
|
+
if not as_dict:
|
|
593
|
+
config_core.configure_logging(root=base, stream=sys.stdout)
|
|
594
|
+
text = format_info_tables(
|
|
595
|
+
study_info,
|
|
596
|
+
width=width,
|
|
597
|
+
float_decimals=float_decimals,
|
|
598
|
+
)
|
|
599
|
+
logger.info("%s", text)
|
|
600
|
+
return None
|
|
601
|
+
return study_info
|
|
602
|
+
|
|
603
|
+
study_info['Scan(s)'] = scan_info
|
|
604
|
+
if not as_dict:
|
|
605
|
+
config_core.configure_logging(root=base, stream=sys.stdout)
|
|
606
|
+
text = format_info_tables(
|
|
607
|
+
study_info,
|
|
608
|
+
width=width,
|
|
609
|
+
scan_indent=1,
|
|
610
|
+
reco_indent=1,
|
|
611
|
+
scan_transpose=scan_transpose,
|
|
612
|
+
float_decimals=float_decimals,
|
|
613
|
+
)
|
|
614
|
+
logger.info("%s", text)
|
|
615
|
+
return None
|
|
616
|
+
return study_info
|
|
617
|
+
|
|
618
|
+
__all__ = [
|
|
619
|
+
"BrukerLoader",
|
|
620
|
+
]
|
|
621
|
+
|
|
622
|
+
__dir__ = make_dir(__all__)
|