From 4797c39241c661fae3c8a686ed15801b2f16af74 Mon Sep 17 00:00:00 2001 From: vitous Date: Sun, 26 Jul 2026 15:29:13 +0200 Subject: [PATCH] Slice-package splitting requires VisuCoreSlicePacksSlices, which PV5.1 never writes (spec 7.10) Fixes #169 --- brukerapi/config/properties_2dseq_core.json | 7 +-- brukerapi/dataset.py | 2 +- brukerapi/splitters.py | 54 ++++++++++++--------- test/test_split.py | 31 +++++++++++- 4 files changed, 62 insertions(+), 32 deletions(-) diff --git a/brukerapi/config/properties_2dseq_core.json b/brukerapi/config/properties_2dseq_core.json index 520f40b..59e121a 100644 --- a/brukerapi/config/properties_2dseq_core.json +++ b/brukerapi/config/properties_2dseq_core.json @@ -161,14 +161,9 @@ ], "num_slice_packages": [ { - "cmd": "len(#VisuCoreSlicePacksSlices.nested)", - "conditions": [] - }, - { - "cmd": "1", + "cmd": "len(@slice_packages_index())", "conditions": [] } - ], "slope": [ { diff --git a/brukerapi/dataset.py b/brukerapi/dataset.py index c6da217..0726c11 100644 --- a/brukerapi/dataset.py +++ b/brukerapi/dataset.py @@ -1204,7 +1204,7 @@ def get_slice_packages(self): """ if self.type != "2dseq": raise UnsupportedDatasetType(f"slice packages are only available for 2dseq, not {self.type}") - if self.num_slice_packages <= 1: + if len(self.slice_packages_index()) <= 1: return [self] from .splitters import SlicePackageSplitter diff --git a/brukerapi/splitters.py b/brukerapi/splitters.py index b258a1b..85d66f2 100644 --- a/brukerapi/splitters.py +++ b/brukerapi/splitters.py @@ -4,7 +4,7 @@ import numpy as np from .dataset import Dataset -from .exceptions import MissingProperty +from .jcampdx import JCAMPDX from .utils import index_to_slice SUPPORTED_FG = ["FG_ISA", "FG_IRMODE", "FG_ECHO"] @@ -286,17 +286,11 @@ def split(self, dataset, write=None, path_out=None): if write is None: write = False - try: - VisuCoreSlicePacksSlices = dataset["VisuCoreSlicePacksSlices"].nested - except KeyError: - raise MissingProperty("Parameter VisuCoreSlicePacksSlices not found") from KeyError + packages = dataset.slice_packages_index() # list of split data sets datasets = [] - # range of frames of given slice package - frame_range = range(0, 0) - # Which frame group holds the slices. Spec 7.10: VisuCoreSlicePacksDef is # (fg_index, num_packages), and an fg_index of -1 is a documented sentinel # meaning there is no slices frame group and every frame belongs to the @@ -313,12 +307,8 @@ def split(self, dataset, write=None, path_out=None): fg_abs_index = fg_rel_index + dataset.encoded_dim # slice package split loop - for sp_index in range(len(VisuCoreSlicePacksSlices)): - # set range - frame_range = range(frame_range.stop, frame_range.stop + VisuCoreSlicePacksSlices[sp_index][1]) - - # number of frames contained in given slice package - frame_count = frame_range.stop - frame_range.start + for sp_index, (first_frame, frame_count) in enumerate(packages): + frame_range = range(first_frame, first_frame + frame_count) # name of the data set created by the split name = f"{dataset.path.parents[0].name}_sp_{sp_index}/2dseq" @@ -373,9 +363,9 @@ def _split_parameters(self, dataset, frame_range, *, fg_rel_index, fg_abs_index, self._split_VisuCoreTransposition(dataset, visu_pars_, frame_range, fg_rel_index) self._split_VisuCoreFrameCount(dataset, visu_pars_, frame_count, fg_abs_index) self._split_VisuFGOrderDesc(visu_pars_, fg_rel_index, frame_count) - self._split_VisuCoreSlicePacksDef(visu_pars_) - self._split_VisuCoreSlicePacksSlices(visu_pars_, sp_index) - self._split_VisuCoreSlicePacksSliceDist(visu_pars_, sp_index) + self._split_VisuCoreSlicePacksDef(visu_pars_, fg_rel_index) + self._split_VisuCoreSlicePacksSlices(visu_pars_, frame_count) + self._split_VisuCoreSlicePacksSliceDist(dataset, visu_pars_, sp_index) return {"visu_pars": visu_pars_} @@ -410,18 +400,34 @@ def _split_VisuFGOrderDesc(self, visu_pars, fg_rel_ind, frame_count): VisuFGOrderDesc.value = value - def _split_VisuCoreSlicePacksDef(self, visu_pars): + @staticmethod + def _synthesise_parameter(visu_pars, key, value): + _, parameter = JCAMPDX.handle_jcampdx_line(f"##${key}={value}", visu_pars.version) + visu_pars.set_parameter(key, parameter) + + def _split_VisuCoreSlicePacksDef(self, visu_pars, fg_rel_index): + if "VisuCoreSlicePacksDef" not in visu_pars: + self._synthesise_parameter(visu_pars, "VisuCoreSlicePacksDef", f"({fg_rel_index}, 1)") + return VisuCoreSlicePacksDef = visu_pars["VisuCoreSlicePacksDef"] value = VisuCoreSlicePacksDef.value value[1] = 1 VisuCoreSlicePacksDef.value = value - def _split_VisuCoreSlicePacksSlices(self, visu_pars_, sp_index): - VisuCoreSlicePacksSlices = visu_pars_["VisuCoreSlicePacksSlices"] - VisuCoreSlicePacksSlices.value = [VisuCoreSlicePacksSlices.value[sp_index]] - - def _split_VisuCoreSlicePacksSliceDist(self, visu_pars_, sp_index): - VisuCoreSlicePacksSliceDist = visu_pars_["VisuCoreSlicePacksSliceDist"] + def _split_VisuCoreSlicePacksSlices(self, visu_pars, frame_count): + if "VisuCoreSlicePacksSlices" not in visu_pars: + self._synthesise_parameter(visu_pars, "VisuCoreSlicePacksSlices", f"( 1 )\n(0, {frame_count})") + return + VisuCoreSlicePacksSlices = visu_pars["VisuCoreSlicePacksSlices"] + VisuCoreSlicePacksSlices.value = [[0, frame_count]] + VisuCoreSlicePacksSlices.size = (1,) + + def _split_VisuCoreSlicePacksSliceDist(self, dataset, visu_pars, sp_index): + if "VisuCoreSlicePacksSliceDist" not in visu_pars: + distance = float(np.linalg.norm(dataset.affine_of_package(sp_index)[:3, 2])) + self._synthesise_parameter(visu_pars, "VisuCoreSlicePacksSliceDist", str(distance)) + return + VisuCoreSlicePacksSliceDist = visu_pars["VisuCoreSlicePacksSliceDist"] # spec 7.10: the inter-slice distance is a double, and truncating it # loses the fractional millimetres of every non-integer slice spacing value = float(VisuCoreSlicePacksSliceDist.array[sp_index]) diff --git a/test/test_split.py b/test/test_split.py index 8aa47db..c92b055 100644 --- a/test/test_split.py +++ b/test/test_split.py @@ -4,7 +4,7 @@ from brukerapi.dataset import Dataset from brukerapi.splitters import FrameGroupSplitter, SlicePackageSplitter, Splitter -from test.synthetic import stacked_positions, write_2dseq +from test.synthetic import axial_orientation, stacked_positions, write_2dseq def test_split_transposition_is_noop_when_parameter_is_absent(): @@ -51,6 +51,35 @@ def test_splitSlicePkg(test_split_data, tmp_path): assert len(datasets) == dataset["VisuCoreSlicePacksSlices"].size[0] +def test_slice_package_splitter_infers_and_synthesises_pv51_packages(tmp_path): + """PV5.1 has no ยง7.10 package parameters; orientation delimits packages.""" + sagittal = np.array([0.0, 1.0, 0.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0]) + positions = np.vstack( + [ + stacked_positions((-20.0, -20.0, -3.0), (0.0, 0.0, 1.5), 2), + stacked_positions((0.0, -20.0, -20.0), (-1.0, 0.0, 0.0), 3), + ] + ) + orientations = np.vstack([axial_orientation(2), np.tile(sagittal, (3, 1))]) + path = write_2dseq( + tmp_path / "9" / "pdata" / "1", + creator_version="5.1", + frame_groups=(("FG_SLICE", 5),), + positions=positions, + orientations=orientations, + ) + + dataset = Dataset(path) + packages = SlicePackageSplitter().split(dataset, write=True) + + assert dataset.num_slice_packages == 2 + assert [package.data.shape[2] for package in packages] == [2, 3] + assert all(package.num_slice_packages == 1 for package in packages) + assert [package["VisuCoreSlicePacksSlices"].nested for package in packages] == [[[0, 2]], [[0, 3]]] + assert [package["VisuCoreSlicePacksDef"].value for package in packages] == [[0, 1], [0, 1]] + assert [Dataset(package.path).num_slice_packages for package in packages] == [1, 1] + + def _echo_dataset(tmp_path, echoes=2, slices=3): path = write_2dseq( tmp_path / "5" / "pdata" / "1",