brkraw 0.5.2__py3-none-any.whl → 0.5.5__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 (39) hide show
  1. brkraw/__init__.py +1 -1
  2. brkraw/api/__init__.py +122 -0
  3. brkraw/api/types.py +39 -0
  4. brkraw/apps/loader/__init__.py +3 -6
  5. brkraw/apps/loader/core.py +128 -132
  6. brkraw/apps/loader/formatter.py +0 -2
  7. brkraw/apps/loader/helper.py +334 -114
  8. brkraw/apps/loader/info/scan.py +2 -2
  9. brkraw/apps/loader/info/transform.py +0 -1
  10. brkraw/apps/loader/types.py +56 -59
  11. brkraw/cli/commands/addon.py +1 -1
  12. brkraw/cli/commands/cache.py +82 -0
  13. brkraw/cli/commands/config.py +2 -2
  14. brkraw/cli/commands/convert.py +61 -38
  15. brkraw/cli/commands/hook.py +1 -3
  16. brkraw/cli/commands/info.py +1 -1
  17. brkraw/cli/commands/init.py +1 -1
  18. brkraw/cli/commands/params.py +1 -1
  19. brkraw/cli/commands/prune.py +2 -2
  20. brkraw/cli/commands/session.py +1 -11
  21. brkraw/cli/main.py +51 -1
  22. brkraw/cli/utils.py +1 -1
  23. brkraw/core/cache.py +87 -0
  24. brkraw/core/config.py +18 -2
  25. brkraw/core/fs.py +26 -9
  26. brkraw/core/zip.py +46 -32
  27. brkraw/dataclasses/__init__.py +3 -2
  28. brkraw/dataclasses/study.py +73 -23
  29. brkraw/resolver/datatype.py +10 -2
  30. brkraw/resolver/image.py +140 -21
  31. brkraw/resolver/nifti.py +4 -12
  32. brkraw/schema/niftiheader.yaml +0 -2
  33. brkraw/specs/meta/validator.py +0 -1
  34. brkraw/specs/rules/logic.py +1 -3
  35. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/METADATA +8 -9
  36. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/RECORD +39 -35
  37. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/entry_points.txt +1 -0
  38. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/WHEEL +0 -0
  39. {brkraw-0.5.2.dist-info → brkraw-0.5.5.dist-info}/licenses/LICENSE +0 -0
brkraw/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = '0.5.2'
3
+ __version__ = '0.5.5'
4
4
  from .apps.loader import BrukerLoader
5
5
 
6
6
 
brkraw/api/__init__.py ADDED
@@ -0,0 +1,122 @@
1
+ """Public API surface for BrkRaw.
2
+
3
+ This module intentionally re-exports a curated set of symbols for external use.
4
+ To keep import-time fast and reduce side effects, most symbols are lazily
5
+ imported on first access (PEP 562: module `__getattr__`).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from importlib import import_module
11
+ from typing import TYPE_CHECKING, Any, Dict, Tuple
12
+
13
+ # Public API -----------------------------------------------------------------
14
+
15
+ __all__ = [
16
+ "formatter",
17
+ "BrukerLoader",
18
+ "loader",
19
+ "hook",
20
+ "hook_manager",
21
+ "hook_resolver",
22
+ "pruner",
23
+ "rules",
24
+ "addon",
25
+ "addon_manager",
26
+ "validate_meta",
27
+ "transform",
28
+ "info_resolver",
29
+ "affine_resolver",
30
+ "shape_resolver",
31
+ "image_resolver",
32
+ "fid_resolver",
33
+ "nifti_resolver",
34
+ "types",
35
+ "config",
36
+ ]
37
+
38
+ # Lazy import map: name -> (module_path, attribute_name or None)
39
+ # If attribute_name is None, the module itself is returned.
40
+ _LAZY: Dict[str, Tuple[str, str | None]] = {
41
+ # core
42
+ "formatter": ("brkraw.core", "formatter"),
43
+
44
+ # apps
45
+ "BrukerLoader": ("brkraw.apps.loader", "BrukerLoader"),
46
+ "loader": ("brkraw.apps", "loader"),
47
+ "hook_manager": ("brkraw.apps", "hook"),
48
+ "addon_manager": ("brkraw.apps", "addon"),
49
+ "hook_resolver": ("brkraw.apps.loader.helper", "resolve_converter_hook"),
50
+ "config": ("brkraw.core", "config"),
51
+
52
+ # apps.loader.info resolvers
53
+ "info_resolver": ("brkraw.apps.loader", "info"),
54
+ "transform": ("brkraw.apps.loader.info", "transform"),
55
+
56
+ # resolvers
57
+ "affine_resolver": ("brkraw.resolver", "affine"),
58
+ "shape_resolver": ("brkraw.resolver", "shape"),
59
+ "image_resolver": ("brkraw.resolver", "image"),
60
+ "fid_resolver": ("brkraw.resolver", "fid"),
61
+ "nifti_resolver": ("brkraw.resolver", "nifti"),
62
+
63
+ # specs
64
+ "hook": ("brkraw.specs", "hook"),
65
+ "pruner": ("brkraw.specs", "pruner"),
66
+ "rules": ("brkraw.specs", "rules"),
67
+ "addon": ("brkraw.specs", "remapper"),
68
+
69
+ # meta
70
+ "validate_meta": ("brkraw.specs.meta", "validate_meta"),
71
+
72
+ # local
73
+ "types": ("brkraw.api", "types"),
74
+ }
75
+
76
+
77
+ def __getattr__(name: str) -> Any:
78
+ """Lazily import and return public symbols.
79
+
80
+ This keeps import-time minimal while preserving the convenient
81
+ `brkraw.api.<symbol>` access pattern.
82
+ """
83
+
84
+ try:
85
+ mod_path, attr = _LAZY[name]
86
+ except KeyError as e:
87
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from e
88
+
89
+ mod = import_module(mod_path)
90
+ obj = mod if attr is None else getattr(mod, attr)
91
+
92
+ # Cache the resolved object on the module for fast subsequent access.
93
+ globals()[name] = obj
94
+ return obj
95
+
96
+
97
+ def __dir__() -> list[str]:
98
+ # Expose the curated public surface in interactive environments.
99
+ return sorted(set(__all__))
100
+
101
+
102
+ # Type-checking / IDE support -------------------------------------------------
103
+ # Importing these only for type-checking keeps runtime imports lazy.
104
+ if TYPE_CHECKING:
105
+ from brkraw.core import formatter as formatter
106
+ from brkraw.core import config as config
107
+ from brkraw.apps.loader import BrukerLoader as BrukerLoader
108
+ from brkraw.apps.loader import info as info_resolver
109
+ from brkraw.apps.loader.info import transform as transform
110
+ from brkraw.apps.loader.helper import resolve_converter_hook as hook_resolver
111
+ from brkraw.apps import addon as addon_manager, hook as hook_manager, loader as loader
112
+ from brkraw.resolver import (
113
+ affine as affine_resolver,
114
+ fid as fid_resolver,
115
+ image as image_resolver,
116
+ nifti as nifti_resolver,
117
+ shape as shape_resolver,
118
+ )
119
+ from brkraw.specs import hook as hook, pruner as pruner, rules as rules
120
+ from brkraw.specs import remapper as addon
121
+ from brkraw.specs.meta import validate_meta as validate_meta
122
+ from brkraw.api import types as types
brkraw/api/types.py ADDED
@@ -0,0 +1,39 @@
1
+ from ..apps.loader.types import (
2
+ StudyLoader,
3
+ ScanLoader,
4
+ RecoLoader
5
+ )
6
+ from ..apps.loader.types import (
7
+ GetAffineType,
8
+ GetDataobjType,
9
+ ConvertType,
10
+ ConverterHook,
11
+ SubjectType,
12
+ SubjectPose,
13
+ AffineSpace,
14
+ Affines,
15
+ Dataobjs,
16
+ Metadata,
17
+ ConvertedObj,
18
+ HookArgs
19
+ )
20
+ from ..core.parameters import Parameters
21
+
22
+ __all__ = [
23
+ "GetAffineType",
24
+ "GetDataobjType",
25
+ "ConvertType",
26
+ "ConverterHook",
27
+ "StudyLoader",
28
+ "ScanLoader",
29
+ "RecoLoader",
30
+ "SubjectType",
31
+ "SubjectPose",
32
+ "AffineSpace",
33
+ "Affines",
34
+ "Dataobjs",
35
+ "Metadata",
36
+ "ConvertedObj",
37
+ "HookArgs",
38
+ "Parameters"
39
+ ]
@@ -1,10 +1,7 @@
1
- """BrkRaw loader package entrypoint.
2
-
3
- Last updated: 2025-12-30
4
- """
5
1
  from __future__ import annotations
6
2
 
7
-
8
3
  from .core import BrukerLoader
9
4
 
10
- __all__ = ["BrukerLoader"]
5
+ __all__ = [
6
+ "BrukerLoader"
7
+ ]
@@ -3,8 +3,6 @@
3
3
  This module binds helper methods onto Scan/Reco objects so callers can fetch
4
4
  reconstructed data, affines, NIfTI images, metadata, and parameter search
5
5
  results with a simple API.
6
-
7
- Last updated: 2025-12-30
8
6
  """
9
7
 
10
8
  from __future__ import annotations
@@ -14,19 +12,26 @@ import re
14
12
  import logging
15
13
  import sys
16
14
  from functools import partial
17
- from typing import TYPE_CHECKING, Optional, Tuple, Union, Any, Mapping, Callable, cast, List, Dict, Iterable, Literal
15
+ from typing import (
16
+ TYPE_CHECKING, cast,
17
+ Optional, Union,
18
+ Any, Iterable,
19
+ Tuple, List, Mapping, Dict, Literal,
20
+ )
18
21
  from pathlib import Path
19
22
 
20
23
  from ...core import config as config_core
21
24
  from ...core.config import resolve_root
22
- from ...specs import hook as converter_core
23
25
  from ...specs.pruner import prune_dataset_to_zip
24
26
  from ...specs.rules import load_rules, select_rule_use
25
- from ...dataclasses import Scan, Study
26
- from .types import StudyLoader, ScanLoader, RecoLoader
27
+ from ...dataclasses import Study
28
+ from .types import (
29
+ StudyLoader,
30
+ ScanLoader,
31
+ RecoLoader,
32
+ )
27
33
  from .formatter import format_info_tables
28
34
 
29
- logger = logging.getLogger("brkraw")
30
35
  from . import info as info_resolver
31
36
  from .helper import (
32
37
  make_dir,
@@ -35,24 +40,39 @@ from .helper import (
35
40
  get_dataobj as _get_dataobj,
36
41
  get_metadata as _get_metadata,
37
42
  get_nifti1image as _get_nifti1image,
43
+ resolve_reco_id as _resolve_reco_id,
38
44
  search_parameters as _search_parameters,
39
45
  apply_converter_hook as _apply_converter_hook,
46
+ resolve_converter_hook as _resolve_converter_hook,
40
47
  resolve_data_and_affine as _resolve_data_and_affine,
41
48
  )
42
49
 
43
50
  if TYPE_CHECKING:
44
51
  import numpy as np
45
52
  from pathlib import Path
53
+ from .types import (
54
+ XYZUNIT,
55
+ TUNIT,
56
+ SubjectType,
57
+ SubjectPose,
58
+ InfoScope,
59
+ AffineSpace,
60
+ Dataobjs,
61
+ Affines,
62
+ ConvertedObj,
63
+ Metadata,
64
+ )
46
65
  from ...resolver.nifti import Nifti1HeaderContents
47
- from .types import XYZUNIT, TUNIT, SubjectType, SubjectPose, InfoScope, AffineSpace
48
66
 
67
+ logger = logging.getLogger(__name__)
49
68
 
50
69
 
51
70
  class BrukerLoader:
52
71
  """High-level entrypoint that resolves scans and exposes handy accessors."""
53
72
 
54
73
  def __init__(self,
55
- path: Union[str, Path],
74
+ path: Union[str, Path],
75
+ disable_hook: bool = False,
56
76
  affine_decimals: Optional[int] = None):
57
77
  """
58
78
  Create a loader for a Bruker study rooted at `path`.
@@ -68,8 +88,11 @@ class BrukerLoader:
68
88
  self._study: Union["Study", "StudyLoader"] = Study.from_path(path)
69
89
  if affine_decimals is None:
70
90
  affine_decimals = config_core.float_decimals(root=resolve_root(None))
91
+ self._base = resolve_root(None)
92
+ self._scans = {}
71
93
  self._affine_decimals = affine_decimals
72
94
  self._sw_version: Optional[str] = self._parse_sw_version()
95
+ self._hook_disabled = disable_hook
73
96
  self._attach_helpers()
74
97
 
75
98
  def _parse_sw_version(self) -> Optional[str]:
@@ -139,74 +162,52 @@ class BrukerLoader:
139
162
 
140
163
  def _attach_helpers(self):
141
164
  """Resolve per-scan metadata and bind helper methods."""
165
+ logger.debug("Attaching helpers to study %s", getattr(self._study.fs, "root", "?"))
142
166
  self._study = cast(StudyLoader, self._study)
143
167
  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():
168
+
169
+ for lazy_scan in self._study.avail.values():
170
+ scan = lazy_scan.materialize()
150
171
  _resolve_data_and_affine(
151
172
  scan,
152
173
  affine_decimals=self._affine_decimals
153
174
  )
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", "?"))
175
+ scan = cast(ScanLoader, lazy_scan.materialize())
176
+ self.reset_converter(scan)
204
177
  scan.get_metadata = MethodType(_get_metadata, scan)
205
178
  scan.search_params = MethodType(_search_parameters, scan)
179
+ scan._hook_resolved = False
180
+
206
181
  for reco in scan.avail.values():
207
182
  reco = cast(RecoLoader, reco)
208
183
  reco.search_params = MethodType(_search_parameters, reco)
209
184
 
185
+ def _prep_scan(self, scan_id: int, reco_id: Optional[int] = None, **kwargs: Any) -> ScanLoader:
186
+ scan = self.get_scan(scan_id)
187
+
188
+ enable_hook = kwargs.get("enable_hook") # force enable
189
+ if enable_hook is not None:
190
+ del kwargs["enable_hook"]
191
+ else:
192
+ enable_hook = False
193
+
194
+ hook_is_enabled = enable_hook or not self._hook_disabled
195
+
196
+ if hook_is_enabled:
197
+ if enable_hook:
198
+ logger.debug("hook enabled by optional argument for get_dataobj()")
199
+ if scan._hook_resolved is False: # prevent multiple execution
200
+ _resolve_converter_hook(scan, self._base, affine_decimals=self._affine_decimals)
201
+
202
+ logger.debug(
203
+ "scan=%s reco=%s hook_enabled=%s hook=%s",
204
+ scan_id,
205
+ reco_id,
206
+ hook_is_enabled,
207
+ getattr(scan, "_converter_hook_name", None),
208
+ )
209
+ return scan
210
+
210
211
  def search_params(self, key: str,
211
212
  *,
212
213
  file: Optional[Union[str, List[str]]] = None,
@@ -226,34 +227,13 @@ class BrukerLoader:
226
227
  self._study = cast(StudyLoader, self._study)
227
228
  return self._study.search_params(key, file=file, scan_id=scan_id, reco_id=reco_id)
228
229
 
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:
230
+ def reset_converter(self, scan: ScanLoader) -> None:
250
231
  """Restore default conversion methods for a scan.
251
232
 
252
233
  Args:
253
234
  scan_id: Scan identifier.
254
235
  """
255
- scan = self.avail[scan_id]
256
- scan = cast(ScanLoader, scan)
236
+ logger.debug("Initializing converter for scan %s", getattr(scan, "scan_id", "?"))
257
237
  scan.get_dataobj = MethodType(_get_dataobj, scan)
258
238
  scan.get_affine = MethodType(
259
239
  partial(_get_affine, decimals=self._affine_decimals),
@@ -262,6 +242,8 @@ class BrukerLoader:
262
242
  scan.get_nifti1image = MethodType(_get_nifti1image, scan)
263
243
  scan.convert = MethodType(_convert, scan)
264
244
  scan._converter_hook = None
245
+ scan._converter_hook_name = None
246
+ scan.converter_func = None
265
247
 
266
248
  def get_scan(self, scan_id: int) -> "ScanLoader":
267
249
  """Return scan by id.
@@ -272,8 +254,7 @@ class BrukerLoader:
272
254
  Returns:
273
255
  Scan loader instance.
274
256
  """
275
- scan = self._study.get_scan(scan_id)
276
- scan = cast(ScanLoader, scan)
257
+ scan = cast(ScanLoader, self._study.get_scan(scan_id))
277
258
  return scan
278
259
 
279
260
  def get_fid(
@@ -298,9 +279,18 @@ class BrukerLoader:
298
279
  scan = self.get_scan(scan_id)
299
280
  if not hasattr(scan, 'get_fid'):
300
281
  return None
301
- return scan.get_fid(buffer_start=buffer_start, buffer_size=buffer_size, as_complex=as_complex)
282
+ return scan.get_fid(
283
+ buffer_start=buffer_start,
284
+ buffer_size=buffer_size,
285
+ as_complex=as_complex
286
+ )
302
287
 
303
- def get_dataobj(self, scan_id: int, reco_id: Optional[int] = None) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
288
+ def get_dataobj(
289
+ self,
290
+ scan_id: int,
291
+ reco_id: Optional[int] = None,
292
+ **kwargs: Any
293
+ ) -> Dataobjs:
304
294
  """Return reconstructed data for a scan/reco via attached helper.
305
295
 
306
296
  Args:
@@ -310,18 +300,20 @@ class BrukerLoader:
310
300
  Returns:
311
301
  Single ndarray when one slice pack exists; otherwise a tuple.
312
302
  """
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"]]:
303
+ scan = self._prep_scan(scan_id, reco_id, **kwargs)
304
+ return scan.get_dataobj(reco_id, **kwargs)
305
+
306
+ def get_affine(
307
+ self,
308
+ scan_id: int,
309
+ reco_id: Optional[int] = None,
310
+ *,
311
+ space: AffineSpace = 'subject_ras',
312
+ override_subject_type: Optional[SubjectType] = None,
313
+ override_subject_pose: Optional[SubjectPose] = None,
314
+ decimals: Optional[int] = None,
315
+ **kwargs: Any
316
+ ) -> Affines:
325
317
  """Return affine(s) for a scan/reco via attached helper.
326
318
 
327
319
  Args:
@@ -335,24 +327,25 @@ class BrukerLoader:
335
327
  Returns:
336
328
  Single affine matrix when one slice pack exists; otherwise a tuple.
337
329
  """
338
- scan = self.get_scan(scan_id)
330
+ scan = self._prep_scan(scan_id, reco_id, **kwargs)
339
331
  decimals = decimals or self._affine_decimals
340
332
  return scan.get_affine(reco_id,
341
333
  space=space,
342
334
  override_subject_pose=override_subject_pose,
343
335
  override_subject_type=override_subject_type,
344
- decimals=decimals)
336
+ decimals=decimals, **kwargs)
345
337
 
346
- def get_nifti1image(self, scan_id: int, reco_id: Optional[int] = None,
338
+ def get_nifti1image(
339
+ self, scan_id: int, reco_id: Optional[int] = None,
347
340
  *,
348
341
  space: AffineSpace = 'subject_ras',
349
342
  override_header: Optional[Nifti1HeaderContents] = None,
350
343
  override_subject_type: Optional[SubjectType] = None,
351
344
  override_subject_pose: Optional[SubjectPose] = None,
352
- flip_x: bool = False,
353
345
  flatten_fg: bool = False,
354
346
  xyz_units: XYZUNIT = 'mm',
355
- t_units: TUNIT = 'sec'):
347
+ t_units: TUNIT = 'sec'
348
+ ) -> ConvertedObj:
356
349
  """Return NIfTI image(s) for a scan/reco via attached helper.
357
350
 
358
351
  Args:
@@ -362,7 +355,6 @@ class BrukerLoader:
362
355
  override_header: Optional header values to apply.
363
356
  override_subject_type: Subject type override for subject view.
364
357
  override_subject_pose: Subject pose override for subject view.
365
- flip_x: If True, set NIfTI header x-flip flag.
366
358
  xyz_units: Spatial units for NIfTI header.
367
359
  t_units: Temporal units for NIfTI header.
368
360
 
@@ -370,15 +362,23 @@ class BrukerLoader:
370
362
  Single NIfTI image when one slice pack exists; otherwise a tuple.
371
363
  """
372
364
  scan = self.get_scan(scan_id)
373
- return scan.convert(
374
- reco_id,
375
- format="nifti",
376
- space=space,
365
+ dataobj = scan.get_dataobj(reco_id)
366
+ affine = scan.get_affine(reco_id,
367
+ space=space,
368
+ decimals=self._affine_decimals,
369
+ override_subject_pose=override_subject_pose,
370
+ override_subject_type=override_subject_type,
371
+ flatten_fg=flatten_fg,
372
+ xyz_units=xyz_units,
373
+ t_units=t_units)
374
+ resolved_reco_id = _resolve_reco_id(scan, reco_id)
375
+ if resolved_reco_id is None:
376
+ return None
377
+ return scan.get_nifti1image(
378
+ resolved_reco_id,
379
+ cast(Tuple[np.ndarray, ...], dataobj),
380
+ cast(Tuple[np.ndarray, ...], affine),
377
381
  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
382
  xyz_units=xyz_units,
383
383
  t_units=t_units,
384
384
  )
@@ -388,31 +388,25 @@ class BrukerLoader:
388
388
  scan_id: int,
389
389
  reco_id: Optional[int] = None,
390
390
  *,
391
- format: Literal["nifti", "nifti1"] = "nifti",
392
391
  space: AffineSpace = 'subject_ras',
393
392
  override_header: Optional[Nifti1HeaderContents] = None,
394
393
  override_subject_type: Optional[SubjectType] = None,
395
394
  override_subject_pose: Optional[SubjectPose] = None,
396
- flip_x: bool = False,
397
395
  flatten_fg: bool = False,
398
- xyz_units: XYZUNIT = "mm",
399
- t_units: TUNIT = "sec",
400
396
  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)
397
+ **kwargs: Any,
398
+ ) -> ConvertedObj:
399
+ """Convert a scan/reco to output object(s) supporting to_filename()."""
400
+ scan = self._prep_scan(scan_id, reco_id, **kwargs)
404
401
  return scan.convert(
405
402
  reco_id,
406
- format=format,
407
403
  space=space,
408
404
  override_header=override_header,
409
405
  override_subject_type=override_subject_type,
410
406
  override_subject_pose=override_subject_pose,
411
- flip_x=flip_x,
412
407
  flatten_fg=flatten_fg,
413
- xyz_units=xyz_units,
414
- t_units=t_units,
415
408
  hook_args_by_name=hook_args_by_name,
409
+ **kwargs,
416
410
  )
417
411
 
418
412
  def get_metadata(
@@ -422,7 +416,7 @@ class BrukerLoader:
422
416
  spec: Optional[Union[Mapping[str, Any], str, Path]] = None,
423
417
  context_map: Optional[Union[str, Path]] = None,
424
418
  return_spec: bool = False,
425
- ):
419
+ ) -> Metadata:
426
420
  """Return metadata for a scan/reco.
427
421
 
428
422
  Args:
@@ -480,9 +474,11 @@ class BrukerLoader:
480
474
  return BrukerLoader(out_path, affine_decimals=self._affine_decimals)
481
475
 
482
476
  @property
483
- def avail(self) -> Mapping[int, Union["Scan", "ScanLoader"]]:
477
+ def avail(self) -> Mapping[int, "ScanLoader"]:
484
478
  """Available scans keyed by scan id."""
485
- return self._study.avail
479
+ if len(self._scans) != len(self._study.avail):
480
+ self._scans = {scan_id: cast(ScanLoader, scan.materialize()) for scan_id, scan in self._study.avail.items()}
481
+ return self._scans
486
482
 
487
483
  @property
488
484
  def subject(self) -> Optional[Dict[str, Any]]:
@@ -1,6 +1,4 @@
1
1
  """Formatting helpers for loader info output.
2
-
3
- Last updated: 2025-12-30
4
2
  """
5
3
 
6
4
  from __future__ import annotations