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,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
|
brkraw/resolver/image.py
ADDED
|
@@ -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
|
+
]
|