brkraw 0.5.0rc1__py3-none-any.whl → 0.5.1__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 (36) hide show
  1. brkraw/__init__.py +1 -1
  2. brkraw/apps/hook/core.py +58 -10
  3. brkraw/apps/loader/core.py +5 -1
  4. brkraw/apps/loader/helper.py +155 -14
  5. brkraw/apps/loader/info/scan.py +18 -5
  6. brkraw/apps/loader/types.py +6 -1
  7. brkraw/cli/commands/convert.py +201 -79
  8. brkraw/cli/commands/hook.py +146 -4
  9. brkraw/cli/commands/session.py +6 -1
  10. brkraw/cli/hook_args.py +80 -0
  11. brkraw/core/config.py +8 -0
  12. brkraw/core/layout.py +56 -11
  13. brkraw/default/rules/00_default.yaml +4 -0
  14. brkraw/default/specs/metadata_dicom.yaml +236 -0
  15. brkraw/default/specs/metadata_transforms.py +18 -0
  16. brkraw/resolver/affine.py +56 -32
  17. brkraw/schema/context_map.yaml +5 -0
  18. brkraw/schema/remapper.yaml +6 -0
  19. brkraw/specs/__init__.py +2 -2
  20. brkraw/specs/{converter → hook}/logic.py +1 -0
  21. brkraw/specs/{converter → hook}/validator.py +1 -0
  22. brkraw/specs/remapper/logic.py +83 -16
  23. brkraw/specs/remapper/validator.py +21 -5
  24. {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.dist-info}/METADATA +31 -4
  25. {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.dist-info}/RECORD +29 -33
  26. brkraw/default/rules/10-metadata.yaml +0 -42
  27. brkraw/default/rules/20-mrs.yaml +0 -14
  28. brkraw/default/specs/metadata_anat.yaml +0 -54
  29. brkraw/default/specs/metadata_common.yaml +0 -129
  30. brkraw/default/specs/metadata_func.yaml +0 -127
  31. brkraw/default/specs/mrs.yaml +0 -71
  32. brkraw/default/specs/mrs_transforms.py +0 -26
  33. brkraw/specs/{converter → hook}/__init__.py +1 -1
  34. {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.dist-info}/WHEEL +0 -0
  35. {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.dist-info}/entry_points.txt +0 -0
  36. {brkraw-0.5.0rc1.dist-info → brkraw-0.5.1.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.0rc1'
3
+ __version__ = '0.5.1'
4
4
  from .apps.loader import BrukerLoader
5
5
 
6
6
 
brkraw/apps/hook/core.py CHANGED
@@ -7,7 +7,7 @@ import importlib.metadata
7
7
  import logging
8
8
  import re
9
9
  from pathlib import Path
10
- from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
10
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union, cast
11
11
 
12
12
  try:
13
13
  from importlib import resources
@@ -17,7 +17,7 @@ import yaml
17
17
 
18
18
  from ...core import config as config_core
19
19
  from ...core.entrypoints import list_entry_points
20
- from ...specs.converter.logic import DEFAULT_GROUP
20
+ from ...specs.hook.logic import DEFAULT_GROUP
21
21
  from ..addon import installation as addon_install
22
22
  from ..addon import dependencies as addon_deps
23
23
  from ..addon.io import write_file as _write_file
@@ -28,6 +28,41 @@ REGISTRY_FILENAME = "hooks.yaml"
28
28
  MANIFEST_NAMES = ("brkraw_hook.yaml", "brkraw_hook.yml")
29
29
 
30
30
 
31
+ def _metadata_get(
32
+ dist: Optional[importlib.metadata.Distribution],
33
+ key: str,
34
+ default: Optional[str] = None,
35
+ ) -> Optional[str]:
36
+ """Typed helper to read distribution metadata."""
37
+ if dist is None:
38
+ return default
39
+ meta = cast(Mapping[str, str], dist.metadata)
40
+ return meta.get(key, default)
41
+
42
+
43
+ def _packages_distributions() -> Mapping[str, List[str]]:
44
+ packages_distributions = getattr(importlib.metadata, "packages_distributions", None)
45
+ if packages_distributions is not None:
46
+ return cast(Callable[[], Mapping[str, List[str]]], packages_distributions)()
47
+
48
+ mapping: Dict[str, List[str]] = {}
49
+ for dist in importlib.metadata.distributions():
50
+ read_text = getattr(dist, "read_text", None)
51
+ top_level_text = ""
52
+ if callable(read_text):
53
+ top_level = read_text("top_level.txt")
54
+ if isinstance(top_level, str):
55
+ top_level_text = top_level
56
+ dist_name = _metadata_get(dist, "Name")
57
+ if not dist_name:
58
+ continue
59
+ for package in top_level_text.splitlines():
60
+ package = package.strip()
61
+ if package:
62
+ mapping.setdefault(package, []).append(dist_name)
63
+ return mapping
64
+
65
+
31
66
  def list_hooks(*, root: Optional[Union[str, Path]] = None) -> List[Dict[str, Any]]:
32
67
  hooks = _collect_hooks()
33
68
  registry = _load_registry(root=root)
@@ -88,13 +123,26 @@ def uninstall_hook(
88
123
  *,
89
124
  root: Optional[Union[str, Path]] = None,
90
125
  force: bool = False,
91
- ) -> Tuple[str, Dict[str, List[str]]]:
126
+ ) -> Tuple[str, Dict[str, List[str]], bool]:
92
127
  registry = _load_registry(root=root)
93
128
  hooks = registry.get("hooks", {})
94
129
  hook_name = _resolve_hook_name(target)
95
130
  entry = hooks.get(hook_name)
131
+ if entry is None:
132
+ entry_matches = [
133
+ name
134
+ for name, data in hooks.items()
135
+ if target in (data.get("entrypoints") or [])
136
+ ]
137
+ if len(entry_matches) == 1:
138
+ hook_name = entry_matches[0]
139
+ entry = hooks.get(hook_name)
140
+ elif entry_matches:
141
+ names = ", ".join(sorted(entry_matches))
142
+ raise ValueError(f"Multiple hooks match {target}: {names}")
96
143
  if entry is None:
97
144
  raise LookupError(f"Hook not installed: {hook_name}")
145
+ module_missing = not list_entry_points(DEFAULT_GROUP, name=hook_name)
98
146
  removed: Dict[str, List[str]] = {
99
147
  "specs": [],
100
148
  "pruner_specs": [],
@@ -113,7 +161,7 @@ def uninstall_hook(
113
161
  removed[kind].append(relpath)
114
162
  hooks.pop(hook_name, None)
115
163
  _save_registry(registry, root=root)
116
- return hook_name, removed
164
+ return hook_name, removed, module_missing
117
165
 
118
166
 
119
167
  def _install_hook(
@@ -347,7 +395,7 @@ def _resolve_distribution(ep: importlib.metadata.EntryPoint) -> Optional[importl
347
395
  pkg = getattr(ep, "module", "").split(".")[0]
348
396
  if not pkg:
349
397
  return None
350
- mapping = importlib.metadata.packages_distributions()
398
+ mapping = _packages_distributions()
351
399
  dist_names = mapping.get(pkg, [])
352
400
  if not dist_names:
353
401
  return None
@@ -360,26 +408,26 @@ def _resolve_distribution(ep: importlib.metadata.EntryPoint) -> Optional[importl
360
408
  def _dist_name(dist: Optional[importlib.metadata.Distribution]) -> Optional[str]:
361
409
  if dist is None:
362
410
  return None
363
- return dist.metadata.get("Name")
411
+ return _metadata_get(dist, "Name")
364
412
 
365
413
 
366
414
  def _dist_version(dist: Optional[importlib.metadata.Distribution]) -> str:
367
415
  if dist is None:
368
416
  return "<Unknown>"
369
- return dist.metadata.get("Version", "<Unknown>")
417
+ return _metadata_get(dist, "Version", "<Unknown>") or "<Unknown>"
370
418
 
371
419
 
372
420
  def _dist_description(dist: Optional[importlib.metadata.Distribution]) -> str:
373
421
  if dist is None:
374
422
  return "<Unknown>"
375
- return dist.metadata.get("Summary", "<Unknown>")
423
+ return _metadata_get(dist, "Summary", "<Unknown>") or "<Unknown>"
376
424
 
377
425
 
378
426
  def _dist_author(dist: Optional[importlib.metadata.Distribution]) -> str:
379
427
  if dist is None:
380
428
  return "<Unknown>"
381
429
  for key in ("Author", "Author-email", "Maintainer", "Maintainer-email"):
382
- value = dist.metadata.get(key)
430
+ value = _metadata_get(dist, key)
383
431
  if value:
384
432
  return value
385
433
  return "<Unknown>"
@@ -394,7 +442,7 @@ def _load_manifest(
394
442
  manifest = _find_manifest(dist, packages=packages)
395
443
  if manifest is None:
396
444
  raise FileNotFoundError(
397
- f"No hook manifest found in {dist.metadata.get('Name', '<Unknown>')}"
445
+ f"No hook manifest found in {_metadata_get(dist, 'Name', '<Unknown>')}"
398
446
  )
399
447
  data = _read_yaml(manifest)
400
448
  return manifest, data
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
 
20
20
  from ...core import config as config_core
21
21
  from ...core.config import resolve_root
22
- from ...specs import converter as converter_core
22
+ from ...specs import hook as converter_core
23
23
  from ...specs.pruner import prune_dataset_to_zip
24
24
  from ...specs.rules import load_rules, select_rule_use
25
25
  from ...dataclasses import Scan, Study
@@ -350,6 +350,7 @@ class BrukerLoader:
350
350
  override_subject_type: Optional[SubjectType] = None,
351
351
  override_subject_pose: Optional[SubjectPose] = None,
352
352
  flip_x: bool = False,
353
+ flatten_fg: bool = False,
353
354
  xyz_units: XYZUNIT = 'mm',
354
355
  t_units: TUNIT = 'sec'):
355
356
  """Return NIfTI image(s) for a scan/reco via attached helper.
@@ -377,6 +378,7 @@ class BrukerLoader:
377
378
  override_subject_type=override_subject_type,
378
379
  override_subject_pose=override_subject_pose,
379
380
  flip_x=flip_x,
381
+ flatten_fg=flatten_fg,
380
382
  xyz_units=xyz_units,
381
383
  t_units=t_units,
382
384
  )
@@ -392,6 +394,7 @@ class BrukerLoader:
392
394
  override_subject_type: Optional[SubjectType] = None,
393
395
  override_subject_pose: Optional[SubjectPose] = None,
394
396
  flip_x: bool = False,
397
+ flatten_fg: bool = False,
395
398
  xyz_units: XYZUNIT = "mm",
396
399
  t_units: TUNIT = "sec",
397
400
  hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
@@ -406,6 +409,7 @@ class BrukerLoader:
406
409
  override_subject_type=override_subject_type,
407
410
  override_subject_pose=override_subject_pose,
408
411
  flip_x=flip_x,
412
+ flatten_fg=flatten_fg,
409
413
  xyz_units=xyz_units,
410
414
  t_units=t_units,
411
415
  hook_args_by_name=hook_args_by_name,
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  from types import MethodType
9
9
  from functools import partial
10
+ import inspect
10
11
  from typing import TYPE_CHECKING, Optional, Tuple, Union, Any, Mapping, cast, List, Dict, Literal
11
12
  from pathlib import Path
12
13
  from warnings import warn
@@ -21,7 +22,7 @@ from ...specs.remapper import load_spec, map_parameters, load_context_map, apply
21
22
  from ...specs.rules import load_rules, select_rule_use
22
23
  from ...dataclasses import Reco, Scan, Study
23
24
  from .types import ScanLoader
24
- from ...specs import converter as converter_core
25
+ from ...specs import hook as converter_core
25
26
  from ...resolver import affine as affine_resolver
26
27
  from ...resolver import image as image_resolver
27
28
  from ...resolver import fid as fid_resolver
@@ -106,11 +107,29 @@ def resolve_data_and_affine(
106
107
  list(scan.avail.keys()),
107
108
  )
108
109
  continue
109
- image_info = image_resolver.resolve(scan, rid)
110
- # store subject-view affines (scanner unwrap happens in get_affine)
111
- affine_info = affine_resolver.resolve(
112
- scan, rid, decimals=affine_decimals, unwrap_pose=False,
113
- )
110
+ try:
111
+ image_info = image_resolver.resolve(scan, rid)
112
+ except Exception as exc:
113
+ logger.warning(
114
+ "Failed to resolve image data for scan %s reco %s: %s",
115
+ getattr(scan, "scan_id", "?"),
116
+ rid,
117
+ exc,
118
+ )
119
+ image_info = None
120
+ try:
121
+ # store subject-view affines (scanner unwrap happens in get_affine)
122
+ affine_info = affine_resolver.resolve(
123
+ scan, rid, decimals=affine_decimals, unwrap_pose=False,
124
+ )
125
+ except Exception as exc:
126
+ logger.warning(
127
+ "Failed to resolve affine for scan %s reco %s: %s",
128
+ getattr(scan, "scan_id", "?"),
129
+ rid,
130
+ exc,
131
+ )
132
+ affine_info = None
114
133
 
115
134
  if hasattr(scan, "image_info"):
116
135
  scan.image_info[rid] = image_info
@@ -327,7 +346,7 @@ def get_affine(
327
346
  override_subject_type: Optional["SubjectType"] = None,
328
347
  override_subject_pose: Optional["SubjectPose"] = None,
329
348
  decimals: Optional[int] = None,
330
- **_: Any,
349
+ **kwargs: Any,
331
350
  ) -> AffineReturn:
332
351
  """
333
352
  Return affine(s) for a reco in the requested coordinate space.
@@ -379,7 +398,8 @@ def get_affine(
379
398
 
380
399
  # "raw" does not need subject info
381
400
  if space == "raw":
382
- return _finalize_affines(affines, num_slice_packs, decimals)
401
+ result = _finalize_affines(affines, num_slice_packs, decimals)
402
+ return _apply_affine_post_transform(result, kwargs=kwargs)
383
403
 
384
404
  # Need subject type/pose for unwrap and wrap
385
405
  visu_pars = get_file(self.avail[resolved_reco_id], "visu_pars")
@@ -392,7 +412,8 @@ def get_affine(
392
412
  ]
393
413
 
394
414
  if space == "scanner":
395
- return _finalize_affines(affines_scanner, num_slice_packs, decimals)
415
+ result = _finalize_affines(affines_scanner, num_slice_packs, decimals)
416
+ return _apply_affine_post_transform(result, kwargs=kwargs)
396
417
 
397
418
  # Step 2: wrap to subject RAS (optionally with override)
398
419
  use_type = override_subject_type or subj_type
@@ -402,7 +423,52 @@ def get_affine(
402
423
  affine_resolver.wrap_to_subject_ras(affine, use_type, use_pose)
403
424
  for affine in affines_scanner
404
425
  ]
405
- return _finalize_affines(affines_subject_ras, num_slice_packs, decimals)
426
+ result = _finalize_affines(affines_subject_ras, num_slice_packs, decimals)
427
+ return _apply_affine_post_transform(result, kwargs=kwargs)
428
+
429
+
430
+ def _apply_affine_post_transform(affines: AffineReturn, *, kwargs: Mapping[str, Any]) -> AffineReturn:
431
+ """Apply optional flips/rotations to affines right before returning.
432
+
433
+ These transforms are applied in world space and do not depend on output
434
+ `space`. They are controlled via extra kwargs (intentionally not strict):
435
+
436
+ - flip_x / flip_y / flip_z: bool-like
437
+ - rad_x / rad_y / rad_z: radians (float-like)
438
+ """
439
+
440
+ def as_bool(value: Any) -> bool:
441
+ if isinstance(value, str):
442
+ return value.strip().lower() in {"1", "true", "yes", "y", "on"}
443
+ return bool(value)
444
+
445
+ def as_float(value: Any) -> float:
446
+ try:
447
+ return float(value)
448
+ except (TypeError, ValueError):
449
+ return 0.0
450
+
451
+ flip_x = as_bool(kwargs.get("flip_x", False))
452
+ flip_y = as_bool(kwargs.get("flip_y", False))
453
+ flip_z = as_bool(kwargs.get("flip_z", False))
454
+ rad_x = as_float(kwargs.get("rad_x", 0.0))
455
+ rad_y = as_float(kwargs.get("rad_y", 0.0))
456
+ rad_z = as_float(kwargs.get("rad_z", 0.0))
457
+
458
+ if not (flip_x or flip_y or flip_z or rad_x or rad_y or rad_z):
459
+ return affines
460
+
461
+ def apply_one(a: np.ndarray) -> np.ndarray:
462
+ out = np.asarray(a, dtype=float)
463
+ if flip_x or flip_y or flip_z:
464
+ out = affine_resolver.flip_affine(out, flip_x=flip_x, flip_y=flip_y, flip_z=flip_z)
465
+ if rad_x or rad_y or rad_z:
466
+ out = affine_resolver.rotate_affine(out, rad_x=rad_x, rad_y=rad_y, rad_z=rad_z)
467
+ return np.asarray(out, dtype=float)
468
+
469
+ if isinstance(affines, tuple):
470
+ return tuple(apply_one(np.asarray(a)) for a in affines)
471
+ return apply_one(np.asarray(affines))
406
472
 
407
473
 
408
474
  def get_nifti1image(
@@ -414,6 +480,7 @@ def get_nifti1image(
414
480
  override_subject_type: Optional[SubjectType] = None,
415
481
  override_subject_pose: Optional[SubjectPose] = None,
416
482
  flip_x: bool = False,
483
+ flatten_fg: bool = False,
417
484
  xyz_units: XYZUNIT = "mm",
418
485
  t_units: TUNIT = "sec",
419
486
  hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
@@ -446,17 +513,19 @@ def get_nifti1image(
446
513
  if resolved_reco_id is None:
447
514
  return None
448
515
  hook_kwargs = _resolve_hook_kwargs(self, hook_args_by_name)
449
- if hook_kwargs:
450
- dataobjs = self.get_dataobj(resolved_reco_id, **hook_kwargs)
516
+ data_kwargs = _filter_hook_kwargs(self.get_dataobj, hook_kwargs)
517
+ if data_kwargs:
518
+ dataobjs = self.get_dataobj(resolved_reco_id, **data_kwargs)
451
519
  else:
452
520
  dataobjs = self.get_dataobj(resolved_reco_id)
453
- if hook_kwargs:
521
+ affine_kwargs = _filter_hook_kwargs(self.get_affine, hook_kwargs)
522
+ if affine_kwargs:
454
523
  affines = self.get_affine(
455
524
  resolved_reco_id,
456
525
  space=space,
457
526
  override_subject_type=override_subject_type,
458
527
  override_subject_pose=override_subject_pose,
459
- **hook_kwargs,
528
+ **affine_kwargs,
460
529
  )
461
530
  else:
462
531
  affines = self.get_affine(
@@ -476,6 +545,10 @@ def get_nifti1image(
476
545
 
477
546
  niiobjs = []
478
547
  for i, dataobj in enumerate(dataobjs):
548
+ if flatten_fg and dataobj.ndim > 4:
549
+ spatial_shape = dataobj.shape[:3]
550
+ flattened = int(np.prod(dataobj.shape[3:]))
551
+ dataobj = dataobj.reshape((*spatial_shape, flattened), order="A")
479
552
  affine = affines[i]
480
553
  niiobj = Nifti1Image(dataobj, affine)
481
554
  nifti1header_contents = nifti_resolver.resolve(
@@ -503,6 +576,7 @@ def convert(
503
576
  override_subject_type: Optional[SubjectType] = None,
504
577
  override_subject_pose: Optional[SubjectPose] = None,
505
578
  flip_x: bool = False,
579
+ flatten_fg: bool = False,
506
580
  xyz_units: XYZUNIT = "mm",
507
581
  t_units: TUNIT = "sec",
508
582
  hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
@@ -518,6 +592,7 @@ def convert(
518
592
  override_subject_type=override_subject_type,
519
593
  override_subject_pose=override_subject_pose,
520
594
  flip_x=flip_x,
595
+ flatten_fg=flatten_fg,
521
596
  xyz_units=xyz_units,
522
597
  t_units=t_units,
523
598
  hook_args_by_name=hook_args_by_name,
@@ -534,9 +609,75 @@ def _resolve_hook_kwargs(
534
609
  if not isinstance(hook_name, str) or not hook_name:
535
610
  return {}
536
611
  values = hook_args_by_name.get(hook_name)
612
+ if values is None:
613
+ seen: set[str] = set()
614
+
615
+ def _add(candidate: str) -> None:
616
+ cand = candidate.strip()
617
+ if not cand or cand in seen:
618
+ return
619
+ seen.add(cand)
620
+
621
+ _add(hook_name)
622
+ _add(hook_name.lower())
623
+ _add(hook_name.replace("_", "-"))
624
+ _add(hook_name.replace("-", "_"))
625
+ _add(hook_name.lower().replace("_", "-"))
626
+ _add(hook_name.lower().replace("-", "_"))
627
+ _add(f"brkraw-{hook_name}")
628
+ _add(f"brkraw_{hook_name}")
629
+ _add(f"brkraw-{hook_name.lower()}")
630
+ _add(f"brkraw_{hook_name.lower()}")
631
+ _add(f"brkraw-{hook_name.lower().replace('_', '-')}")
632
+ _add(f"brkraw_{hook_name.lower().replace('-', '_')}")
633
+
634
+ for candidate in sorted(seen):
635
+ if candidate == hook_name:
636
+ continue
637
+ candidate_values = hook_args_by_name.get(candidate)
638
+ if candidate_values is not None:
639
+ logger.debug(
640
+ "Using hook args for %r from alias %r.",
641
+ hook_name,
642
+ candidate,
643
+ )
644
+ values = candidate_values
645
+ break
537
646
  return dict(values) if isinstance(values, Mapping) else {}
538
647
 
539
648
 
649
+ def _filter_hook_kwargs(func: Any, hook_kwargs: Mapping[str, Any]) -> Dict[str, Any]:
650
+ """Drop unsupported hook kwargs for a callable.
651
+
652
+ This keeps YAML/CLI presets safe when converter hooks do not accept
653
+ arbitrary kwargs.
654
+ """
655
+ if not hook_kwargs:
656
+ return {}
657
+ try:
658
+ sig = inspect.signature(func)
659
+ except (TypeError, ValueError):
660
+ return dict(hook_kwargs)
661
+ for param in sig.parameters.values():
662
+ if param.kind == inspect.Parameter.VAR_KEYWORD:
663
+ return dict(hook_kwargs)
664
+ allowed = {
665
+ param.name
666
+ for param in sig.parameters.values()
667
+ if param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
668
+ and param.name != "self"
669
+ }
670
+ filtered = {key: value for key, value in hook_kwargs.items() if key in allowed}
671
+ dropped = [key for key in hook_kwargs.keys() if key not in allowed]
672
+ if dropped:
673
+ logger.debug(
674
+ "Ignoring unsupported hook args for %s: %s",
675
+ getattr(func, "__name__", "<callable>"),
676
+ ", ".join(sorted(dropped)),
677
+ )
678
+ return filtered
679
+
680
+
540
681
  def _resolve_metadata_spec(
541
682
  scan: "ScanLoader",
542
683
  spec: Optional[Union[Mapping[str, Any], str, Path]],
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, cast, TYPE_CHECKING, Dict, Optional, Union
4
4
  from pathlib import Path
5
+ import logging
5
6
  from ....specs.remapper import load_spec, map_parameters
6
7
  from ....specs.remapper.validator import validate_spec
7
8
 
@@ -9,6 +10,8 @@ from ....specs.remapper.validator import validate_spec
9
10
  if TYPE_CHECKING:
10
11
  from ..types import ScanLoader, RecoLoader
11
12
 
13
+ logger = logging.getLogger("brkraw")
14
+
12
15
 
13
16
  def resolve(
14
17
  scan: "ScanLoader",
@@ -59,14 +62,24 @@ def resolve(
59
62
  if len(scan.avail):
60
63
  results['Reco(s)'] = {}
61
64
  for reco_id in scan.avail.keys():
62
- reco_spec = {"Type": {"sources": [
65
+ reco_spec = {
66
+ "Type": {
67
+ "sources": [
63
68
  {
64
- 'file': "visu_pars",
65
- 'key': "VisuCoreFrameType",
66
- 'reco_id': reco_id
69
+ "file": "visu_pars",
70
+ "key": "VisuCoreFrameType",
71
+ "reco_id": reco_id,
67
72
  }
68
73
  ]
69
74
  }
70
75
  }
71
- results['Reco(s)'][reco_id] = map_parameters(scan, reco_spec)
76
+ try:
77
+ results["Reco(s)"][reco_id] = map_parameters(scan, reco_spec)
78
+ except (FileNotFoundError, AttributeError) as exc:
79
+ logger.warning(
80
+ "visu_pars missing for scan %s reco %s; skipping reco entry: %s",
81
+ getattr(scan, "scan_id", "unknown"),
82
+ reco_id,
83
+ exc,
84
+ )
72
85
  return results
@@ -75,6 +75,7 @@ class GetNifti1ImageType(Protocol):
75
75
  override_subject_type: Optional[SubjectType],
76
76
  override_subject_pose: Optional[SubjectPose],
77
77
  flip_x: bool,
78
+ flatten_fg: bool,
78
79
  xyz_units: XYZUNIT,
79
80
  t_units: TUNIT,
80
81
  **kwargs: Any,
@@ -95,6 +96,7 @@ class ConvertType(Protocol):
95
96
  override_subject_type: Optional[SubjectType],
96
97
  override_subject_pose: Optional[SubjectPose],
97
98
  flip_x: bool,
99
+ flatten_fg: bool,
98
100
  xyz_units: XYZUNIT,
99
101
  t_units: TUNIT,
100
102
  **kwargs: Any,
@@ -147,7 +149,8 @@ class ScanLoader(Scan, BaseLoader):
147
149
  space: AffineSpace = "subject_ras",
148
150
  override_subject_type: Optional[SubjectType],
149
151
  override_subject_pose: Optional[SubjectPose],
150
- decimals: Optional[int] = None
152
+ decimals: Optional[int] = None,
153
+ **kwargs: Any,
151
154
  ) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
152
155
  ...
153
156
 
@@ -160,6 +163,7 @@ class ScanLoader(Scan, BaseLoader):
160
163
  override_subject_type: Optional[SubjectType],
161
164
  override_subject_pose: Optional[SubjectPose],
162
165
  flip_x: bool,
166
+ flatten_fg: bool,
163
167
  xyz_units: XYZUNIT,
164
168
  t_units: TUNIT
165
169
  ) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
@@ -175,6 +179,7 @@ class ScanLoader(Scan, BaseLoader):
175
179
  override_subject_type: Optional[SubjectType],
176
180
  override_subject_pose: Optional[SubjectPose],
177
181
  flip_x: bool,
182
+ flatten_fg: bool,
178
183
  xyz_units: XYZUNIT,
179
184
  t_units: TUNIT,
180
185
  hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,