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.
Files changed (113) hide show
  1. brkraw/__init__.py +9 -3
  2. brkraw/apps/__init__.py +12 -0
  3. brkraw/apps/addon/__init__.py +30 -0
  4. brkraw/apps/addon/core.py +35 -0
  5. brkraw/apps/addon/dependencies.py +402 -0
  6. brkraw/apps/addon/installation.py +500 -0
  7. brkraw/apps/addon/io.py +21 -0
  8. brkraw/apps/hook/__init__.py +25 -0
  9. brkraw/apps/hook/core.py +636 -0
  10. brkraw/apps/loader/__init__.py +10 -0
  11. brkraw/apps/loader/core.py +622 -0
  12. brkraw/apps/loader/formatter.py +288 -0
  13. brkraw/apps/loader/helper.py +797 -0
  14. brkraw/apps/loader/info/__init__.py +11 -0
  15. brkraw/apps/loader/info/scan.py +85 -0
  16. brkraw/apps/loader/info/scan.yaml +90 -0
  17. brkraw/apps/loader/info/study.py +69 -0
  18. brkraw/apps/loader/info/study.yaml +156 -0
  19. brkraw/apps/loader/info/transform.py +92 -0
  20. brkraw/apps/loader/types.py +220 -0
  21. brkraw/cli/__init__.py +5 -0
  22. brkraw/cli/commands/__init__.py +2 -0
  23. brkraw/cli/commands/addon.py +327 -0
  24. brkraw/cli/commands/config.py +205 -0
  25. brkraw/cli/commands/convert.py +903 -0
  26. brkraw/cli/commands/hook.py +348 -0
  27. brkraw/cli/commands/info.py +74 -0
  28. brkraw/cli/commands/init.py +214 -0
  29. brkraw/cli/commands/params.py +106 -0
  30. brkraw/cli/commands/prune.py +288 -0
  31. brkraw/cli/commands/session.py +371 -0
  32. brkraw/cli/hook_args.py +80 -0
  33. brkraw/cli/main.py +83 -0
  34. brkraw/cli/utils.py +60 -0
  35. brkraw/core/__init__.py +13 -0
  36. brkraw/core/config.py +380 -0
  37. brkraw/core/entrypoints.py +25 -0
  38. brkraw/core/formatter.py +367 -0
  39. brkraw/core/fs.py +495 -0
  40. brkraw/core/jcamp.py +600 -0
  41. brkraw/core/layout.py +451 -0
  42. brkraw/core/parameters.py +781 -0
  43. brkraw/core/zip.py +1121 -0
  44. brkraw/dataclasses/__init__.py +14 -0
  45. brkraw/dataclasses/node.py +139 -0
  46. brkraw/dataclasses/reco.py +33 -0
  47. brkraw/dataclasses/scan.py +61 -0
  48. brkraw/dataclasses/study.py +131 -0
  49. brkraw/default/__init__.py +3 -0
  50. brkraw/default/pruner_specs/deid4share.yaml +42 -0
  51. brkraw/default/rules/00_default.yaml +4 -0
  52. brkraw/default/specs/metadata_dicom.yaml +236 -0
  53. brkraw/default/specs/metadata_transforms.py +92 -0
  54. brkraw/resolver/__init__.py +7 -0
  55. brkraw/resolver/affine.py +539 -0
  56. brkraw/resolver/datatype.py +69 -0
  57. brkraw/resolver/fid.py +90 -0
  58. brkraw/resolver/helpers.py +36 -0
  59. brkraw/resolver/image.py +188 -0
  60. brkraw/resolver/nifti.py +370 -0
  61. brkraw/resolver/shape.py +235 -0
  62. brkraw/schema/__init__.py +3 -0
  63. brkraw/schema/context_map.yaml +62 -0
  64. brkraw/schema/meta.yaml +57 -0
  65. brkraw/schema/niftiheader.yaml +95 -0
  66. brkraw/schema/pruner.yaml +55 -0
  67. brkraw/schema/remapper.yaml +128 -0
  68. brkraw/schema/rules.yaml +154 -0
  69. brkraw/specs/__init__.py +10 -0
  70. brkraw/specs/hook/__init__.py +12 -0
  71. brkraw/specs/hook/logic.py +31 -0
  72. brkraw/specs/hook/validator.py +22 -0
  73. brkraw/specs/meta/__init__.py +5 -0
  74. brkraw/specs/meta/validator.py +156 -0
  75. brkraw/specs/pruner/__init__.py +15 -0
  76. brkraw/specs/pruner/logic.py +361 -0
  77. brkraw/specs/pruner/validator.py +119 -0
  78. brkraw/specs/remapper/__init__.py +27 -0
  79. brkraw/specs/remapper/logic.py +924 -0
  80. brkraw/specs/remapper/validator.py +314 -0
  81. brkraw/specs/rules/__init__.py +6 -0
  82. brkraw/specs/rules/logic.py +263 -0
  83. brkraw/specs/rules/validator.py +103 -0
  84. brkraw-0.5.0.dist-info/METADATA +81 -0
  85. brkraw-0.5.0.dist-info/RECORD +88 -0
  86. {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info}/WHEEL +1 -2
  87. brkraw-0.5.0.dist-info/entry_points.txt +13 -0
  88. brkraw/lib/__init__.py +0 -4
  89. brkraw/lib/backup.py +0 -641
  90. brkraw/lib/bids.py +0 -0
  91. brkraw/lib/errors.py +0 -125
  92. brkraw/lib/loader.py +0 -1220
  93. brkraw/lib/orient.py +0 -194
  94. brkraw/lib/parser.py +0 -48
  95. brkraw/lib/pvobj.py +0 -301
  96. brkraw/lib/reference.py +0 -245
  97. brkraw/lib/utils.py +0 -471
  98. brkraw/scripts/__init__.py +0 -0
  99. brkraw/scripts/brk_backup.py +0 -106
  100. brkraw/scripts/brkraw.py +0 -744
  101. brkraw/ui/__init__.py +0 -0
  102. brkraw/ui/config.py +0 -17
  103. brkraw/ui/main_win.py +0 -214
  104. brkraw/ui/previewer.py +0 -225
  105. brkraw/ui/scan_info.py +0 -72
  106. brkraw/ui/scan_list.py +0 -73
  107. brkraw/ui/subj_info.py +0 -128
  108. brkraw-0.3.11.dist-info/METADATA +0 -25
  109. brkraw-0.3.11.dist-info/RECORD +0 -28
  110. brkraw-0.3.11.dist-info/entry_points.txt +0 -3
  111. brkraw-0.3.11.dist-info/top_level.txt +0 -2
  112. tests/__init__.py +0 -0
  113. {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,69 @@
1
+ """
2
+ Utilities to resolve NumPy dtype and scaling parameters for Paravision scans/recos.
3
+
4
+ Given a `Scan` or `Reco` object, `resolve()` inspects the relevant parameter
5
+ objects (`acqp` for Scan, `visu_pars` for Reco) and returns a dict containing:
6
+ - dtype: NumPy dtype built from byte order and word type
7
+ - slope: VisuCoreDataSlope
8
+ - offset: VisuCoreDataOffs
9
+ """
10
+ from __future__ import annotations
11
+ from typing import Union, Optional, TypedDict, cast
12
+ import numpy as np
13
+ from .helpers import get_file
14
+ from ..dataclasses import Scan, Reco
15
+
16
+
17
+ WORDTYPE = {
18
+ "_32BIT_SGN_INT": "i",
19
+ "_16BIT_SGN_INT": "h",
20
+ "_8BIT_UNSGN_INT": "B",
21
+ "_32BIT_FLOAT": "f",
22
+ }
23
+
24
+ BYTEORDER = {
25
+ "littleEndian": "<",
26
+ "bigEndian": ">",
27
+ }
28
+
29
+
30
+ class ResolvedDatatype(TypedDict):
31
+ dtype: np.dtype
32
+ slope: Optional[float]
33
+ offset: Optional[float]
34
+
35
+
36
+
37
+ def _get_dtype(byte_order: str, word_type: str) -> np.dtype:
38
+ if byte_order not in BYTEORDER:
39
+ raise ValueError(f"Unsupported byte order: {byte_order!r}")
40
+ if word_type not in WORDTYPE:
41
+ raise ValueError(f"Unsupported word type: {word_type!r}")
42
+ return np.dtype(f"{BYTEORDER[byte_order]}{WORDTYPE[word_type]}")
43
+
44
+
45
+ def resolve(obj: Union["Scan", "Reco"]) -> Optional[ResolvedDatatype]:
46
+ """Return dtype/slope/offset metadata for a Scan or Reco."""
47
+ if isinstance(obj, Scan):
48
+ try:
49
+ p = get_file(obj, 'acqp')
50
+ except FileNotFoundError:
51
+ return None
52
+ byte_order = f'{p.get("BYTORDA")}Endian'
53
+ word_type = f'_{"".join(p["ACQ_word_size"].split("_"))}_SGN_INT'
54
+ elif isinstance(obj, Reco):
55
+ try:
56
+ p = get_file(obj, 'visu_pars')
57
+ except FileNotFoundError:
58
+ return None
59
+ byte_order = p.get('VisuCoreByteOrder')
60
+ word_type = p.get('VisuCoreWordType')
61
+ else:
62
+ raise TypeError(f"resolve() expects Scan or Reco, got {type(obj)!r}")
63
+ result: ResolvedDatatype = {
64
+ "dtype": _get_dtype(byte_order, word_type),
65
+ "slope": cast(Optional[float], p.get('VisuCoreDataSlope')),
66
+ "offset": cast(Optional[float], p.get('VisuCoreDataOffs')),
67
+ }
68
+
69
+ return result
brkraw/resolver/fid.py ADDED
@@ -0,0 +1,90 @@
1
+ """
2
+ Locate and load Bruker Paravision FID/rawdata for custom reconstruction.
3
+
4
+ This helper finds the FID (or rawdata) file in a Scan node, resolves the
5
+ expected NumPy dtype from metadata, and returns a flat NumPy array. Optional
6
+ byte offsets/sizes allow partial reads for debugging or incremental loading.
7
+ Returns None when required files or dtype metadata are missing.
8
+ """
9
+ from __future__ import annotations
10
+
11
+
12
+ from warnings import warn
13
+ from .datatype import resolve as datatype_resolver
14
+ from typing import TYPE_CHECKING, Optional
15
+
16
+ if TYPE_CHECKING:
17
+ from ..dataclasses import Scan
18
+
19
+ import numpy as np
20
+
21
+ def get_fid(scan: "Scan"):
22
+ """Return the first FID/rawdata candidate in a scan, warning on multiples."""
23
+ fid_candidates = []
24
+ for fileobj in scan.iterdir():
25
+ if 'fid' in fileobj.name or 'rawdata' in fileobj.name:
26
+ fid_candidates.append(fileobj)
27
+ if len(fid_candidates) == 0:
28
+ return None
29
+ elif len(fid_candidates) > 1:
30
+ warn('Multiple FID file candidates found. Take first one.')
31
+ return fid_candidates[0]
32
+
33
+
34
+ def resolve(
35
+ scan: "Scan",
36
+ buffer_start: Optional[int] = None,
37
+ buffer_size: Optional[int] = None,
38
+ *,
39
+ as_complex: bool = True,
40
+ ) -> Optional[np.ndarray]:
41
+ """Load FID as a NumPy array for reconstruction workflows.
42
+
43
+ Args:
44
+ scan: Scan node containing the FID/rawdata file.
45
+ buffer_start: Optional byte offset to start reading (default: 0).
46
+ buffer_size: Optional number of bytes to read (default: entire file).
47
+ as_complex: When True (default), interpret interleaved real/imag pairs
48
+ as complex samples. When False, return the raw 1D array.
49
+
50
+ Returns:
51
+ 1D NumPy array of complex samples (or raw when as_complex=False), or
52
+ None when file/dtype is missing.
53
+
54
+ Raises:
55
+ ValueError: If buffer_start or buffer_size is negative.
56
+ """
57
+ fid_entry = get_fid(scan)
58
+ if fid_entry is None:
59
+ return None
60
+
61
+ dtype_info = datatype_resolver(scan)
62
+ if not dtype_info or 'dtype' not in dtype_info:
63
+ return None
64
+
65
+ dtype = np.dtype(dtype_info['dtype'])
66
+ start = 0 if buffer_start is None else int(buffer_start)
67
+ size = None if buffer_size is None else int(buffer_size)
68
+
69
+ if start < 0 or (size is not None and size < 0):
70
+ raise ValueError("buffer_start and buffer_size must be non-negative")
71
+
72
+ with fid_entry.open() as f:
73
+ f.seek(start)
74
+ raw = f.read() if size is None else f.read(size)
75
+ data = np.frombuffer(raw, dtype)
76
+
77
+ if not as_complex:
78
+ return data
79
+
80
+ if data.size % 2 != 0:
81
+ raise ValueError("FID data length is not even; cannot form complex pairs.")
82
+
83
+ real = data[0::2]
84
+ imag = data[1::2]
85
+ return real.astype(np.float32, copy=False) + 1j * imag.astype(np.float32, copy=False)
86
+
87
+
88
+ __all__ = [
89
+ 'resolve'
90
+ ]
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ from typing import TYPE_CHECKING, Union, Tuple, List, Any
5
+ if TYPE_CHECKING:
6
+ from ..dataclasses import Study, Scan, Reco
7
+
8
+
9
+ def get_reco(obj: "Scan", reco_id):
10
+ if reco_id not in obj.avail.keys():
11
+ raise KeyError('reco_id')
12
+ return obj.avail[reco_id]
13
+
14
+
15
+ def get_file(obj: Union["Study", "Scan", "Reco"], basename: str):
16
+ if not hasattr(obj, basename):
17
+ raise FileNotFoundError(basename)
18
+ else:
19
+ key = obj._full_path(basename)
20
+ if key in obj._cache.keys():
21
+ obj._cache.pop(key, None)
22
+ return getattr(obj, f'file_{basename}')
23
+
24
+ def return_alt_val_if_none(val: object, alt_val: object) -> object:
25
+ if val is None:
26
+ return alt_val
27
+ return val
28
+
29
+ def strip_comment(raw_comment: str):
30
+ return raw_comment.strip('<>').strip()
31
+
32
+
33
+ def swap_element(obj: List[Any], index1: int, index2: int) -> List[Any]:
34
+ new_obj = obj[:]
35
+ new_obj[index1], new_obj[index2] = new_obj[index2], new_obj[index1]
36
+ return new_obj
@@ -0,0 +1,188 @@
1
+ """
2
+ Load Paravision `2dseq` data into a NumPy array with geometry metadata.
3
+
4
+ This resolver reads dtype/slope/offset and shape info for a given `Scan`/`Reco`,
5
+ then reshapes the `2dseq` buffer (Fortran order) and normalizes axis labels so
6
+ the spatial z-axis sits at index 2. Returns None when required metadata or
7
+ files are missing.
8
+ """
9
+ from __future__ import annotations
10
+
11
+
12
+ from typing import TYPE_CHECKING, Optional, Sequence, TypedDict, List, Tuple
13
+ from .datatype import resolve as datatype_resolver
14
+ from .shape import resolve as shape_resolver
15
+ from .helpers import get_reco, get_file, swap_element
16
+ import numpy as np
17
+
18
+ if TYPE_CHECKING:
19
+ from ..dataclasses import Scan, Reco
20
+ from .shape import ResolvedShape
21
+ from .shape import ResolvedCycle
22
+
23
+
24
+ class ResolvedImage(TypedDict):
25
+ dataobj: np.ndarray
26
+ slope: float
27
+ offset: float
28
+ shape_desc: List[str]
29
+ sliceorder_scheme: Optional[str]
30
+ num_cycles: int
31
+ time_per_cycle: Optional[float]
32
+
33
+
34
+ Z_AXIS_DESCRIPTORS = {'spatial', 'slice', 'without_slice'}
35
+
36
+
37
+ def _find_z_axis_candidate(shape_desc: Sequence[str]) -> Optional[int]:
38
+ """Return the first spatial z-axis descriptor index found at/after position 2."""
39
+ for idx, desc in enumerate(shape_desc):
40
+ if idx < 2:
41
+ continue
42
+ if desc in Z_AXIS_DESCRIPTORS:
43
+ return idx
44
+ return None
45
+
46
+
47
+ def _normalize_zaxis_descriptor(shape_desc: List[str]) -> List[str]:
48
+ """Ensure the z-axis descriptor uses 'slice' to represent spatial depth."""
49
+ normalized = shape_desc[:]
50
+ if normalized[2] == 'without_slice':
51
+ normalized[2] = 'slice'
52
+ return normalized
53
+
54
+
55
+ def _validate_swapped_axes(
56
+ dataobj: np.ndarray,
57
+ expected_shape: Sequence[int],
58
+ shape_desc: List[str],
59
+ original_zaxis_desc: str,
60
+ swapped_idx: int,
61
+ ):
62
+ """Validate shape/descriptor invariants after moving spatial z-axis into position 2."""
63
+ if dataobj.shape != tuple(expected_shape):
64
+ raise ValueError(f"data shape {dataobj.shape} does not match expected {tuple(expected_shape)} after z-axis swap")
65
+ if len(expected_shape) != len(shape_desc):
66
+ raise ValueError("shape and shape_desc length mismatch after z-axis normalization")
67
+ if shape_desc[swapped_idx] != original_zaxis_desc:
68
+ raise ValueError(f"axis {swapped_idx} descriptor mismatch after swap; expected '{original_zaxis_desc}'")
69
+ if shape_desc[2] not in Z_AXIS_DESCRIPTORS:
70
+ raise ValueError(f"z-axis descriptor '{shape_desc[2]}' is invalid; expected one of {sorted(Z_AXIS_DESCRIPTORS)}")
71
+
72
+
73
+ def ensure_3d_spatial_data(dataobj: np.ndarray, shape_info: "ResolvedShape") -> Tuple[np.ndarray, List[str]]:
74
+ """
75
+ Normalize data and descriptors so the spatial z-axis sits at index 2.
76
+
77
+ Swaps axes when needed to place the first spatial z-axis descriptor at
78
+ position 2 and rewrites 'without_slice' to 'slice' for clarity.
79
+
80
+ Raises:
81
+ ValueError: When data dimensionality and shape_desc disagree or z-axis
82
+ descriptor is missing.
83
+ """
84
+ shape = shape_info['shape']
85
+ shape_desc = list(shape_info['shape_desc'])
86
+
87
+ if dataobj.ndim != len(shape_desc):
88
+ raise ValueError(f"dataobj.ndim ({dataobj.ndim}) and shape_desc length ({len(shape_desc)}) do not match")
89
+
90
+ if dataobj.ndim < 3 or len(shape_desc) < 3:
91
+ return dataobj, shape_desc
92
+
93
+ if shape_desc[2] in Z_AXIS_DESCRIPTORS:
94
+ return dataobj, _normalize_zaxis_descriptor(shape_desc)
95
+
96
+ zaxis_candi_idx = _find_z_axis_candidate(shape_desc)
97
+ if zaxis_candi_idx is None:
98
+ raise ValueError(f"z-axis descriptor not found in shape_desc starting at index 2: {shape_desc}")
99
+
100
+ pre_zaxis_desc = shape_desc[2]
101
+ new_dataobj = np.swapaxes(dataobj, 2, zaxis_candi_idx)
102
+ new_shape = swap_element(shape, 2, zaxis_candi_idx)
103
+ new_shape_desc = swap_element(shape_desc, 2, zaxis_candi_idx)
104
+
105
+ _validate_swapped_axes(new_dataobj, new_shape, new_shape_desc, pre_zaxis_desc, zaxis_candi_idx)
106
+
107
+ normalized_shape_desc = _normalize_zaxis_descriptor(new_shape_desc)
108
+ return new_dataobj, normalized_shape_desc
109
+
110
+
111
+ def _read_2dseq_data(reco: "Reco", dtype: np.dtype, shape: Sequence[int]) -> np.ndarray:
112
+ """Read 2dseq file into a Fortran-ordered NumPy array with shape validation."""
113
+ expected_size = int(np.prod(shape)) * np.dtype(dtype).itemsize
114
+ with get_file(reco, "2dseq") as f:
115
+ f.seek(0)
116
+ raw = f.read()
117
+ if len(raw) != expected_size:
118
+ raise ValueError(f"2dseq size mismatch: expected {expected_size} bytes for shape {shape}, got {len(raw)}")
119
+ try:
120
+ return np.frombuffer(raw, dtype).reshape(shape, order="F")
121
+ except ValueError as exc:
122
+ raise ValueError(f"failed to reshape 2dseq buffer to shape {shape}") from exc
123
+
124
+
125
+ def _normalize_cycle_info(cycle_info: Optional["ResolvedCycle"]) -> Tuple[int, Optional[float]]:
126
+ """Normalize cycle info and provide safe defaults when metadata is absent."""
127
+ if not cycle_info:
128
+ return 1, None
129
+ return int(cycle_info['num_cycles']), cycle_info.get('time_step')
130
+
131
+
132
+ def resolve(scan: "Scan", reco_id: int = 1) -> Optional[ResolvedImage]:
133
+ """Load 2dseq as a NumPy array with associated metadata.
134
+
135
+ Args:
136
+ scan: Scan node containing the target reco.
137
+ reco_id: Reco identifier to read (default: 1).
138
+
139
+ Returns:
140
+ ImageResolveResult with:
141
+ - dataobj: NumPy array reshaped using Fortran order.
142
+ - slope/offset: intensity scaling.
143
+ - shape_desc: normalized descriptors with spatial z-axis at index 2.
144
+ - slice/cycle metadata.
145
+ None if required metadata or files are missing; raises ValueError on
146
+ inconsistent metadata.
147
+ """
148
+ reco: "Reco" = get_reco(scan, reco_id)
149
+
150
+ dtype_info = datatype_resolver(reco)
151
+ shape_info = shape_resolver(scan, reco_id=reco_id)
152
+ if not dtype_info or not shape_info:
153
+ return None
154
+
155
+ dtype = np.dtype(dtype_info["dtype"])
156
+ slope = dtype_info["slope"]
157
+ if slope is None:
158
+ slope = 1.0
159
+ offset = dtype_info["offset"]
160
+ if offset is None:
161
+ offset = 0.0
162
+ shape = shape_info["shape"]
163
+
164
+ try:
165
+ dataobj = _read_2dseq_data(reco, dtype, shape)
166
+ except FileNotFoundError:
167
+ return None
168
+
169
+ dataobj, shape_desc = ensure_3d_spatial_data(dataobj, shape_info)
170
+ num_cycles, time_per_cycle = _normalize_cycle_info(shape_info['objs'].cycle)
171
+
172
+ result: ResolvedImage = {
173
+ # image
174
+ 'dataobj': dataobj,
175
+ 'slope': slope,
176
+ 'offset': offset,
177
+ 'shape_desc': shape_desc,
178
+ 'sliceorder_scheme': shape_info['sliceorder_scheme'],
179
+
180
+ # cycle
181
+ 'num_cycles': num_cycles,
182
+ 'time_per_cycle': time_per_cycle,
183
+ }
184
+ return result
185
+
186
+ __all__ = [
187
+ 'resolve'
188
+ ]